| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.widget; |
| |
| import com.android.internal.util.FastMath; |
| import com.android.internal.widget.EditableInputConnection; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.res.ColorStateList; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.content.res.XmlResourceParser; |
| import android.graphics.Canvas; |
| import android.graphics.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.ResultReceiver; |
| import android.os.SystemClock; |
| import android.text.BoringLayout; |
| import android.text.ClipboardManager; |
| 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.TextPaint; |
| import android.text.TextUtils; |
| import android.text.TextWatcher; |
| 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.style.ParagraphStyle; |
| 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.ContextMenu; |
| import android.view.Gravity; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.MenuItem; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewDebug; |
| import android.view.ViewGroup.LayoutParams; |
| import android.view.ViewRoot; |
| import android.view.ViewTreeObserver; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityManager; |
| import android.view.animation.AnimationUtils; |
| import android.view.inputmethod.BaseInputConnection; |
| import android.view.inputmethod.CompletionInfo; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.ExtractedText; |
| import android.view.inputmethod.ExtractedTextRequest; |
| import android.view.inputmethod.InputConnection; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.RemoteViews.RemoteView; |
| |
| import java.io.IOException; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| |
| /** |
| * Displays text to the user and optionally allows them to edit it. A TextView |
| * is a complete text editor, however the basic class is configured to not |
| * allow editing; see {@link EditText} for a subclass that configures the text |
| * view for editing. |
| * |
| * <p> |
| * <b>XML attributes</b> |
| * <p> |
| * See {@link android.R.styleable#TextView TextView Attributes}, |
| * {@link android.R.styleable#View View Attributes} |
| * |
| * @attr ref android.R.styleable#TextView_text |
| * @attr ref android.R.styleable#TextView_bufferType |
| * @attr ref android.R.styleable#TextView_hint |
| * @attr ref android.R.styleable#TextView_textColor |
| * @attr ref android.R.styleable#TextView_textColorHighlight |
| * @attr ref android.R.styleable#TextView_textColorHint |
| * @attr ref android.R.styleable#TextView_textAppearance |
| * @attr ref android.R.styleable#TextView_textColorLink |
| * @attr ref android.R.styleable#TextView_textSize |
| * @attr ref android.R.styleable#TextView_textScaleX |
| * @attr ref android.R.styleable#TextView_typeface |
| * @attr ref android.R.styleable#TextView_textStyle |
| * @attr ref android.R.styleable#TextView_cursorVisible |
| * @attr ref android.R.styleable#TextView_maxLines |
| * @attr ref android.R.styleable#TextView_maxHeight |
| * @attr ref android.R.styleable#TextView_lines |
| * @attr ref android.R.styleable#TextView_height |
| * @attr ref android.R.styleable#TextView_minLines |
| * @attr ref android.R.styleable#TextView_minHeight |
| * @attr ref android.R.styleable#TextView_maxEms |
| * @attr ref android.R.styleable#TextView_maxWidth |
| * @attr ref android.R.styleable#TextView_ems |
| * @attr ref android.R.styleable#TextView_width |
| * @attr ref android.R.styleable#TextView_minEms |
| * @attr ref android.R.styleable#TextView_minWidth |
| * @attr ref android.R.styleable#TextView_gravity |
| * @attr ref android.R.styleable#TextView_scrollHorizontally |
| * @attr ref android.R.styleable#TextView_password |
| * @attr ref android.R.styleable#TextView_singleLine |
| * @attr ref android.R.styleable#TextView_selectAllOnFocus |
| * @attr ref android.R.styleable#TextView_includeFontPadding |
| * @attr ref android.R.styleable#TextView_maxLength |
| * @attr ref android.R.styleable#TextView_shadowColor |
| * @attr ref android.R.styleable#TextView_shadowDx |
| * @attr ref android.R.styleable#TextView_shadowDy |
| * @attr ref android.R.styleable#TextView_shadowRadius |
| * @attr ref android.R.styleable#TextView_autoLink |
| * @attr ref android.R.styleable#TextView_linksClickable |
| * @attr ref android.R.styleable#TextView_numeric |
| * @attr ref android.R.styleable#TextView_digits |
| * @attr ref android.R.styleable#TextView_phoneNumber |
| * @attr ref android.R.styleable#TextView_inputMethod |
| * @attr ref android.R.styleable#TextView_capitalize |
| * @attr ref android.R.styleable#TextView_autoText |
| * @attr ref android.R.styleable#TextView_editable |
| * @attr ref android.R.styleable#TextView_freezesText |
| * @attr ref android.R.styleable#TextView_ellipsize |
| * @attr ref android.R.styleable#TextView_drawableTop |
| * @attr ref android.R.styleable#TextView_drawableBottom |
| * @attr ref android.R.styleable#TextView_drawableRight |
| * @attr ref android.R.styleable#TextView_drawableLeft |
| * @attr ref android.R.styleable#TextView_drawablePadding |
| * @attr ref android.R.styleable#TextView_lineSpacingExtra |
| * @attr ref android.R.styleable#TextView_lineSpacingMultiplier |
| * @attr ref android.R.styleable#TextView_marqueeRepeatLimit |
| * @attr ref android.R.styleable#TextView_inputType |
| * @attr ref android.R.styleable#TextView_imeOptions |
| * @attr ref android.R.styleable#TextView_privateImeOptions |
| * @attr ref android.R.styleable#TextView_imeActionLabel |
| * @attr ref android.R.styleable#TextView_imeActionId |
| * @attr ref android.R.styleable#TextView_editorExtras |
| */ |
| @RemoteView |
| public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { |
| static final String LOG_TAG = "TextView"; |
| static final boolean DEBUG_EXTRACT = false; |
| |
| private static int PRIORITY = 100; |
| |
| private ColorStateList mTextColor; |
| private int mCurTextColor; |
| private ColorStateList mHintTextColor; |
| private ColorStateList mLinkTextColor; |
| private int mCurHintTextColor; |
| private boolean mFreezesText; |
| private boolean mFrozenWithFocus; |
| private boolean mTemporaryDetach; |
| private boolean mDispatchTemporaryDetach; |
| |
| private boolean mEatTouchRelease = false; |
| private boolean mScrolled = false; |
| |
| private Editable.Factory mEditableFactory = Editable.Factory.getInstance(); |
| private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance(); |
| |
| private float mShadowRadius, mShadowDx, mShadowDy; |
| |
| private static final int PREDRAW_NOT_REGISTERED = 0; |
| private static final int PREDRAW_PENDING = 1; |
| private static final int PREDRAW_DONE = 2; |
| private int mPreDrawState = PREDRAW_NOT_REGISTERED; |
| |
| private TextUtils.TruncateAt mEllipsize = null; |
| |
| // Enum for the "typeface" XML parameter. |
| // TODO: How can we get this from the XML instead of hardcoding it here? |
| private static final int SANS = 1; |
| private static final int SERIF = 2; |
| private static final int MONOSPACE = 3; |
| |
| // Bitfield for the "numeric" XML parameter. |
| // TODO: How can we get this from the XML instead of hardcoding it here? |
| private static final int SIGNED = 2; |
| private static final int DECIMAL = 4; |
| |
| class Drawables { |
| final Rect mCompoundRect = new Rect(); |
| Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight; |
| int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; |
| int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; |
| int mDrawablePadding; |
| } |
| private Drawables mDrawables; |
| |
| private CharSequence mError; |
| private boolean mErrorWasChanged; |
| private ErrorPopup mPopup; |
| /** |
| * This flag is set if the TextView tries to display an error before it |
| * is attached to the window (so its position is still unknown). |
| * It causes the error to be shown later, when onAttachedToWindow() |
| * is called. |
| */ |
| private boolean mShowErrorAfterAttach; |
| |
| private CharWrapper mCharWrapper = null; |
| |
| private boolean mSelectionMoved = false; |
| private boolean mTouchFocusSelected = false; |
| |
| private Marquee mMarquee; |
| private boolean mRestartMarquee; |
| |
| private int mMarqueeRepeatLimit = 3; |
| |
| class InputContentType { |
| int imeOptions = EditorInfo.IME_NULL; |
| String privateImeOptions; |
| CharSequence imeActionLabel; |
| int imeActionId; |
| Bundle extras; |
| OnEditorActionListener onEditorActionListener; |
| boolean enterDown; |
| } |
| InputContentType mInputContentType; |
| |
| class InputMethodState { |
| Rect mCursorRectInWindow = new Rect(); |
| RectF mTmpRectF = new RectF(); |
| float[] mTmpOffset = new float[2]; |
| ExtractedTextRequest mExtracting; |
| final ExtractedText mTmpExtracted = new ExtractedText(); |
| int mBatchEditNesting; |
| boolean mCursorChanged; |
| boolean mSelectionModeChanged; |
| boolean mContentChanged; |
| int mChangedStart, mChangedEnd, mChangedDelta; |
| } |
| InputMethodState mInputMethodState; |
| |
| /* |
| * Kick-start the font cache for the zygote process (to pay the cost of |
| * initializing freetype for our default font only once). |
| */ |
| static { |
| Paint p = new Paint(); |
| p.setAntiAlias(true); |
| // We don't care about the result, just the side-effect of measuring. |
| p.measureText("H"); |
| } |
| |
| /** |
| * Interface definition for a callback to be invoked when an action is |
| * performed on the editor. |
| */ |
| public interface OnEditorActionListener { |
| /** |
| * Called when an action is being performed. |
| * |
| * @param v The view that was clicked. |
| * @param actionId Identifier of the action. This will be either the |
| * identifier you supplied, or {@link EditorInfo#IME_NULL |
| * EditorInfo.IME_NULL} if being called due to the enter key |
| * being pressed. |
| * @param event If triggered by an enter key, this is the event; |
| * otherwise, this is null. |
| * @return Return true if you have consumed the action, else false. |
| */ |
| boolean onEditorAction(TextView v, int actionId, KeyEvent event); |
| } |
| |
| public TextView(Context context) { |
| this(context, null); |
| } |
| |
| public TextView(Context context, |
| AttributeSet attrs) { |
| this(context, attrs, com.android.internal.R.attr.textViewStyle); |
| } |
| |
| @SuppressWarnings("deprecation") |
| public TextView(Context context, |
| AttributeSet attrs, |
| int defStyle) { |
| super(context, attrs, defStyle); |
| mText = ""; |
| |
| mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); |
| mTextPaint.density = getResources().getDisplayMetrics().density; |
| mTextPaint.setCompatibilityScaling( |
| getResources().getCompatibilityInfo().applicationScale); |
| |
| // If we get the paint from the skin, we should set it to left, since |
| // the layout always wants it to be left. |
| // mTextPaint.setTextAlign(Paint.Align.LEFT); |
| |
| mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mHighlightPaint.setCompatibilityScaling( |
| getResources().getCompatibilityInfo().applicationScale); |
| |
| mMovement = getDefaultMovementMethod(); |
| mTransformation = null; |
| |
| TypedArray a = |
| context.obtainStyledAttributes( |
| attrs, com.android.internal.R.styleable.TextView, defStyle, 0); |
| |
| int textColorHighlight = 0; |
| ColorStateList textColor = null; |
| ColorStateList textColorHint = null; |
| ColorStateList textColorLink = null; |
| int textSize = 15; |
| int typefaceIndex = -1; |
| int styleIndex = -1; |
| |
| /* |
| * Look the appearance up without checking first if it exists because |
| * almost every TextView has one and it greatly simplifies the logic |
| * to be able to parse the appearance first and then let specific tags |
| * for this View override it. |
| */ |
| TypedArray appearance = null; |
| int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1); |
| if (ap != -1) { |
| appearance = context.obtainStyledAttributes(ap, |
| com.android.internal.R.styleable. |
| TextAppearance); |
| } |
| if (appearance != null) { |
| int n = appearance.getIndexCount(); |
| for (int i = 0; i < n; i++) { |
| int attr = appearance.getIndex(i); |
| |
| switch (attr) { |
| case com.android.internal.R.styleable.TextAppearance_textColorHighlight: |
| textColorHighlight = appearance.getColor(attr, textColorHighlight); |
| break; |
| |
| case com.android.internal.R.styleable.TextAppearance_textColor: |
| textColor = appearance.getColorStateList(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextAppearance_textColorHint: |
| textColorHint = appearance.getColorStateList(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextAppearance_textColorLink: |
| textColorLink = appearance.getColorStateList(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextAppearance_textSize: |
| textSize = appearance.getDimensionPixelSize(attr, textSize); |
| break; |
| |
| case com.android.internal.R.styleable.TextAppearance_typeface: |
| typefaceIndex = appearance.getInt(attr, -1); |
| break; |
| |
| case com.android.internal.R.styleable.TextAppearance_textStyle: |
| styleIndex = appearance.getInt(attr, -1); |
| break; |
| } |
| } |
| |
| appearance.recycle(); |
| } |
| |
| boolean editable = getDefaultEditable(); |
| CharSequence inputMethod = null; |
| int numeric = 0; |
| CharSequence digits = null; |
| boolean phone = false; |
| boolean autotext = false; |
| int autocap = -1; |
| int buffertype = 0; |
| boolean selectallonfocus = false; |
| Drawable drawableLeft = null, drawableTop = null, drawableRight = null, |
| drawableBottom = null; |
| int drawablePadding = 0; |
| int ellipsize = -1; |
| boolean singleLine = false; |
| int maxlength = -1; |
| CharSequence text = ""; |
| CharSequence hint = null; |
| int shadowcolor = 0; |
| float dx = 0, dy = 0, r = 0; |
| boolean password = false; |
| int inputType = EditorInfo.TYPE_NULL; |
| |
| int n = a.getIndexCount(); |
| for (int i = 0; i < n; i++) { |
| int attr = a.getIndex(i); |
| |
| switch (attr) { |
| case com.android.internal.R.styleable.TextView_editable: |
| editable = a.getBoolean(attr, editable); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_inputMethod: |
| inputMethod = a.getText(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_numeric: |
| numeric = a.getInt(attr, numeric); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_digits: |
| digits = a.getText(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_phoneNumber: |
| phone = a.getBoolean(attr, phone); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_autoText: |
| autotext = a.getBoolean(attr, autotext); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_capitalize: |
| autocap = a.getInt(attr, autocap); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_bufferType: |
| buffertype = a.getInt(attr, buffertype); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_selectAllOnFocus: |
| selectallonfocus = a.getBoolean(attr, selectallonfocus); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_autoLink: |
| mAutoLinkMask = a.getInt(attr, 0); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_linksClickable: |
| mLinksClickable = a.getBoolean(attr, true); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_drawableLeft: |
| drawableLeft = a.getDrawable(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_drawableTop: |
| drawableTop = a.getDrawable(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_drawableRight: |
| drawableRight = a.getDrawable(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_drawableBottom: |
| drawableBottom = a.getDrawable(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_drawablePadding: |
| drawablePadding = a.getDimensionPixelSize(attr, drawablePadding); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_maxLines: |
| setMaxLines(a.getInt(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_maxHeight: |
| setMaxHeight(a.getDimensionPixelSize(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_lines: |
| setLines(a.getInt(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_height: |
| setHeight(a.getDimensionPixelSize(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_minLines: |
| setMinLines(a.getInt(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_minHeight: |
| setMinHeight(a.getDimensionPixelSize(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_maxEms: |
| setMaxEms(a.getInt(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_maxWidth: |
| setMaxWidth(a.getDimensionPixelSize(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_ems: |
| setEms(a.getInt(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_width: |
| setWidth(a.getDimensionPixelSize(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_minEms: |
| setMinEms(a.getInt(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_minWidth: |
| setMinWidth(a.getDimensionPixelSize(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_gravity: |
| setGravity(a.getInt(attr, -1)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_hint: |
| hint = a.getText(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_text: |
| text = a.getText(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_scrollHorizontally: |
| if (a.getBoolean(attr, false)) { |
| setHorizontallyScrolling(true); |
| } |
| break; |
| |
| case com.android.internal.R.styleable.TextView_singleLine: |
| singleLine = a.getBoolean(attr, singleLine); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_ellipsize: |
| ellipsize = a.getInt(attr, ellipsize); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_marqueeRepeatLimit: |
| setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_includeFontPadding: |
| if (!a.getBoolean(attr, true)) { |
| setIncludeFontPadding(false); |
| } |
| break; |
| |
| case com.android.internal.R.styleable.TextView_cursorVisible: |
| if (!a.getBoolean(attr, true)) { |
| setCursorVisible(false); |
| } |
| break; |
| |
| case com.android.internal.R.styleable.TextView_maxLength: |
| maxlength = a.getInt(attr, -1); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_textScaleX: |
| setTextScaleX(a.getFloat(attr, 1.0f)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_freezesText: |
| mFreezesText = a.getBoolean(attr, false); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_shadowColor: |
| shadowcolor = a.getInt(attr, 0); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_shadowDx: |
| dx = a.getFloat(attr, 0); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_shadowDy: |
| dy = a.getFloat(attr, 0); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_shadowRadius: |
| r = a.getFloat(attr, 0); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_enabled: |
| setEnabled(a.getBoolean(attr, isEnabled())); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_textColorHighlight: |
| textColorHighlight = a.getColor(attr, textColorHighlight); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_textColor: |
| textColor = a.getColorStateList(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_textColorHint: |
| textColorHint = a.getColorStateList(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_textColorLink: |
| textColorLink = a.getColorStateList(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_textSize: |
| textSize = a.getDimensionPixelSize(attr, textSize); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_typeface: |
| typefaceIndex = a.getInt(attr, typefaceIndex); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_textStyle: |
| styleIndex = a.getInt(attr, styleIndex); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_password: |
| password = a.getBoolean(attr, password); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_lineSpacingExtra: |
| mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_lineSpacingMultiplier: |
| mSpacingMult = a.getFloat(attr, mSpacingMult); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_inputType: |
| inputType = a.getInt(attr, mInputType); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_imeOptions: |
| if (mInputContentType == null) { |
| mInputContentType = new InputContentType(); |
| } |
| mInputContentType.imeOptions = a.getInt(attr, |
| mInputContentType.imeOptions); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_imeActionLabel: |
| if (mInputContentType == null) { |
| mInputContentType = new InputContentType(); |
| } |
| mInputContentType.imeActionLabel = a.getText(attr); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_imeActionId: |
| if (mInputContentType == null) { |
| mInputContentType = new InputContentType(); |
| } |
| mInputContentType.imeActionId = a.getInt(attr, |
| mInputContentType.imeActionId); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_privateImeOptions: |
| setPrivateImeOptions(a.getString(attr)); |
| break; |
| |
| case com.android.internal.R.styleable.TextView_editorExtras: |
| try { |
| setInputExtras(a.getResourceId(attr, 0)); |
| } catch (XmlPullParserException e) { |
| Log.w(LOG_TAG, "Failure reading input extras", e); |
| } catch (IOException e) { |
| Log.w(LOG_TAG, "Failure reading input extras", e); |
| } |
| break; |
| } |
| } |
| a.recycle(); |
| |
| BufferType bufferType = BufferType.EDITABLE; |
| |
| if ((inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION)) |
| == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { |
| password = true; |
| } |
| |
| if (inputMethod != null) { |
| Class<?> c; |
| |
| try { |
| c = Class.forName(inputMethod.toString()); |
| } catch (ClassNotFoundException ex) { |
| throw new RuntimeException(ex); |
| } |
| |
| try { |
| mInput = (KeyListener) c.newInstance(); |
| } catch (InstantiationException ex) { |
| throw new RuntimeException(ex); |
| } catch (IllegalAccessException ex) { |
| throw new RuntimeException(ex); |
| } |
| try { |
| mInputType = inputType != EditorInfo.TYPE_NULL |
| ? inputType |
| : mInput.getInputType(); |
| } catch (IncompatibleClassChangeError e) { |
| mInputType = EditorInfo.TYPE_CLASS_TEXT; |
| } |
| } else if (digits != null) { |
| mInput = DigitsKeyListener.getInstance(digits.toString()); |
| // If no input type was specified, we will default to generic |
| // text, since we can't tell the IME about the set of digits |
| // that was selected. |
| mInputType = inputType != EditorInfo.TYPE_NULL |
| ? inputType : EditorInfo.TYPE_CLASS_TEXT; |
| } else if (inputType != EditorInfo.TYPE_NULL) { |
| setInputType(inputType, true); |
| singleLine = (inputType&(EditorInfo.TYPE_MASK_CLASS |
| | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) != |
| (EditorInfo.TYPE_CLASS_TEXT |
| | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); |
| } else if (phone) { |
| mInput = DialerKeyListener.getInstance(); |
| mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE; |
| } else if (numeric != 0) { |
| mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0, |
| (numeric & DECIMAL) != 0); |
| inputType = EditorInfo.TYPE_CLASS_NUMBER; |
| if ((numeric & SIGNED) != 0) { |
| inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED; |
| } |
| if ((numeric & DECIMAL) != 0) { |
| inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; |
| } |
| mInputType = inputType; |
| } else if (autotext || autocap != -1) { |
| TextKeyListener.Capitalize cap; |
| |
| inputType = EditorInfo.TYPE_CLASS_TEXT; |
| if (!singleLine) { |
| inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; |
| } |
| |
| switch (autocap) { |
| case 1: |
| cap = TextKeyListener.Capitalize.SENTENCES; |
| inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; |
| break; |
| |
| case 2: |
| cap = TextKeyListener.Capitalize.WORDS; |
| inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; |
| break; |
| |
| case 3: |
| cap = TextKeyListener.Capitalize.CHARACTERS; |
| inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS; |
| break; |
| |
| default: |
| cap = TextKeyListener.Capitalize.NONE; |
| break; |
| } |
| |
| mInput = TextKeyListener.getInstance(autotext, cap); |
| mInputType = inputType; |
| } else if (editable) { |
| mInput = TextKeyListener.getInstance(); |
| mInputType = EditorInfo.TYPE_CLASS_TEXT; |
| if (!singleLine) { |
| mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; |
| } |
| } else { |
| mInput = null; |
| |
| switch (buffertype) { |
| case 0: |
| bufferType = BufferType.NORMAL; |
| break; |
| case 1: |
| bufferType = BufferType.SPANNABLE; |
| break; |
| case 2: |
| bufferType = BufferType.EDITABLE; |
| break; |
| } |
| } |
| |
| if (password && (mInputType&EditorInfo.TYPE_MASK_CLASS) |
| == EditorInfo.TYPE_CLASS_TEXT) { |
| mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) |
| | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; |
| } |
| |
| if (selectallonfocus) { |
| mSelectAllOnFocus = true; |
| |
| if (bufferType == BufferType.NORMAL) |
| bufferType = BufferType.SPANNABLE; |
| } |
| |
| setCompoundDrawablesWithIntrinsicBounds( |
| drawableLeft, drawableTop, drawableRight, drawableBottom); |
| setCompoundDrawablePadding(drawablePadding); |
| |
| if (singleLine) { |
| setSingleLine(); |
| |
| if (mInput == null && ellipsize < 0) { |
| ellipsize = 3; // END |
| } |
| } |
| |
| switch (ellipsize) { |
| case 1: |
| setEllipsize(TextUtils.TruncateAt.START); |
| break; |
| case 2: |
| setEllipsize(TextUtils.TruncateAt.MIDDLE); |
| break; |
| case 3: |
| setEllipsize(TextUtils.TruncateAt.END); |
| break; |
| case 4: |
| setHorizontalFadingEdgeEnabled(true); |
| setEllipsize(TextUtils.TruncateAt.MARQUEE); |
| break; |
| } |
| |
| setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000)); |
| setHintTextColor(textColorHint); |
| setLinkTextColor(textColorLink); |
| if (textColorHighlight != 0) { |
| setHighlightColor(textColorHighlight); |
| } |
| setRawTextSize(textSize); |
| |
| if (password) { |
| setTransformationMethod(PasswordTransformationMethod.getInstance()); |
| typefaceIndex = MONOSPACE; |
| } else if ((mInputType&(EditorInfo.TYPE_MASK_CLASS |
| |EditorInfo.TYPE_MASK_VARIATION)) |
| == (EditorInfo.TYPE_CLASS_TEXT |
| |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { |
| typefaceIndex = MONOSPACE; |
| } |
| |
| setTypefaceByIndex(typefaceIndex, styleIndex); |
| |
| if (shadowcolor != 0) { |
| setShadowLayer(r, dx, dy, shadowcolor); |
| } |
| |
| if (maxlength >= 0) { |
| setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) }); |
| } else { |
| setFilters(NO_FILTERS); |
| } |
| |
| setText(text, bufferType); |
| if (hint != null) setHint(hint); |
| |
| /* |
| * Views are not normally focusable unless specified to be. |
| * However, TextViews that have input or movement methods *are* |
| * focusable by default. |
| */ |
| a = context.obtainStyledAttributes(attrs, |
| com.android.internal.R.styleable.View, |
| defStyle, 0); |
| |
| boolean focusable = mMovement != null || mInput != null; |
| boolean clickable = focusable; |
| boolean longClickable = focusable; |
| |
| n = a.getIndexCount(); |
| for (int i = 0; i < n; i++) { |
| int attr = a.getIndex(i); |
| |
| switch (attr) { |
| case com.android.internal.R.styleable.View_focusable: |
| focusable = a.getBoolean(attr, focusable); |
| break; |
| |
| case com.android.internal.R.styleable.View_clickable: |
| clickable = a.getBoolean(attr, clickable); |
| break; |
| |
| case com.android.internal.R.styleable.View_longClickable: |
| longClickable = a.getBoolean(attr, longClickable); |
| break; |
| } |
| } |
| a.recycle(); |
| |
| setFocusable(focusable); |
| setClickable(clickable); |
| setLongClickable(longClickable); |
| |
| prepareCursorControllers(); |
| } |
| |
| private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { |
| Typeface tf = null; |
| switch (typefaceIndex) { |
| case SANS: |
| tf = Typeface.SANS_SERIF; |
| break; |
| |
| case SERIF: |
| tf = Typeface.SERIF; |
| break; |
| |
| case MONOSPACE: |
| tf = Typeface.MONOSPACE; |
| break; |
| } |
| |
| setTypeface(tf, styleIndex); |
| } |
| |
| /** |
| * Sets the typeface and style in which the text should be displayed, |
| * and turns on the fake bold and italic bits in the Paint if the |
| * Typeface that you provided does not have all the bits in the |
| * style that you specified. |
| * |
| * @attr ref android.R.styleable#TextView_typeface |
| * @attr ref android.R.styleable#TextView_textStyle |
| */ |
| public void setTypeface(Typeface tf, int style) { |
| if (style > 0) { |
| if (tf == null) { |
| tf = Typeface.defaultFromStyle(style); |
| } else { |
| tf = Typeface.create(tf, style); |
| } |
| |
| setTypeface(tf); |
| // now compute what (if any) algorithmic styling is needed |
| int typefaceStyle = tf != null ? tf.getStyle() : 0; |
| int need = style & ~typefaceStyle; |
| mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); |
| mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); |
| } else { |
| mTextPaint.setFakeBoldText(false); |
| mTextPaint.setTextSkewX(0); |
| setTypeface(tf); |
| } |
| } |
| |
| /** |
| * Subclasses override this to specify that they have a KeyListener |
| * by default even if not specifically called for in the XML options. |
| */ |
| protected boolean getDefaultEditable() { |
| return false; |
| } |
| |
| /** |
| * Subclasses override this to specify a default movement method. |
| */ |
| protected MovementMethod getDefaultMovementMethod() { |
| return null; |
| } |
| |
| /** |
| * Return the text the TextView is displaying. If setText() was called with |
| * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast |
| * the return value from this method to Spannable or Editable, respectively. |
| * |
| * Note: The content of the return value should not be modified. If you want |
| * a modifiable one, you should make your own copy first. |
| */ |
| @ViewDebug.CapturedViewProperty |
| public CharSequence getText() { |
| return mText; |
| } |
| |
| /** |
| * Returns the length, in characters, of the text managed by this TextView |
| */ |
| public int length() { |
| return mText.length(); |
| } |
| |
| /** |
| * Return the text the TextView is displaying as an Editable object. If |
| * the text is not editable, null is returned. |
| * |
| * @see #getText |
| */ |
| public Editable getEditableText() { |
| return (mText instanceof Editable) ? (Editable)mText : null; |
| } |
| |
| /** |
| * @return the height of one standard line in pixels. Note that markup |
| * within the text can cause individual lines to be taller or shorter |
| * than this height, and the layout may contain additional first- |
| * or last-line padding. |
| */ |
| public int getLineHeight() { |
| return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult |
| + mSpacingAdd); |
| } |
| |
| /** |
| * @return the Layout that is currently being used to display the text. |
| * This can be null if the text or width has recently changes. |
| */ |
| public final Layout getLayout() { |
| return mLayout; |
| } |
| |
| /** |
| * @return the current key listener for this TextView. |
| * This will frequently be null for non-EditText TextViews. |
| */ |
| public final KeyListener getKeyListener() { |
| return mInput; |
| } |
| |
| /** |
| * Sets the key listener to be used with this TextView. This can be null |
| * to disallow user input. Note that this method has significant and |
| * subtle interactions with soft keyboards and other input method: |
| * see {@link KeyListener#getInputType() KeyListener.getContentType()} |
| * for important details. Calling this method will replace the current |
| * content type of the text view with the content type returned by the |
| * key listener. |
| * <p> |
| * Be warned that if you want a TextView with a key listener or movement |
| * method not to be focusable, or if you want a TextView without a |
| * key listener or movement method to be focusable, you must call |
| * {@link #setFocusable} again after calling this to get the focusability |
| * back the way you want it. |
| * |
| * @attr ref android.R.styleable#TextView_numeric |
| * @attr ref android.R.styleable#TextView_digits |
| * @attr ref android.R.styleable#TextView_phoneNumber |
| * @attr ref android.R.styleable#TextView_inputMethod |
| * @attr ref android.R.styleable#TextView_capitalize |
| * @attr ref android.R.styleable#TextView_autoText |
| */ |
| public void setKeyListener(KeyListener input) { |
| setKeyListenerOnly(input); |
| fixFocusableAndClickableSettings(); |
| |
| if (input != null) { |
| try { |
| mInputType = mInput.getInputType(); |
| } catch (IncompatibleClassChangeError e) { |
| mInputType = EditorInfo.TYPE_CLASS_TEXT; |
| } |
| if ((mInputType&EditorInfo.TYPE_MASK_CLASS) |
| == EditorInfo.TYPE_CLASS_TEXT) { |
| if (mSingleLine) { |
| mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; |
| } else { |
| mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; |
| } |
| } |
| } else { |
| mInputType = EditorInfo.TYPE_NULL; |
| } |
| |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| if (imm != null) imm.restartInput(this); |
| } |
| |
| private void setKeyListenerOnly(KeyListener input) { |
| mInput = input; |
| if (mInput != null && !(mText instanceof Editable)) |
| setText(mText); |
| |
| setFilters((Editable) mText, mFilters); |
| } |
| |
| /** |
| * @return the movement method being used for this TextView. |
| * This will frequently be null for non-EditText TextViews. |
| */ |
| public final MovementMethod getMovementMethod() { |
| return mMovement; |
| } |
| |
| /** |
| * Sets the movement method (arrow key handler) to be used for |
| * this TextView. This can be null to disallow using the arrow keys |
| * to move the cursor or scroll the view. |
| * <p> |
| * Be warned that if you want a TextView with a key listener or movement |
| * method not to be focusable, or if you want a TextView without a |
| * key listener or movement method to be focusable, you must call |
| * {@link #setFocusable} again after calling this to get the focusability |
| * back the way you want it. |
| */ |
| public final void setMovementMethod(MovementMethod movement) { |
| mMovement = movement; |
| |
| if (mMovement != null && !(mText instanceof Spannable)) |
| setText(mText); |
| |
| fixFocusableAndClickableSettings(); |
| |
| // SelectionModifierCursorController depends on canSelectText, which depends on mMovement |
| prepareCursorControllers(); |
| } |
| |
| private void fixFocusableAndClickableSettings() { |
| if ((mMovement != null) || mInput != null) { |
| setFocusable(true); |
| setClickable(true); |
| setLongClickable(true); |
| } else { |
| setFocusable(false); |
| setClickable(false); |
| setLongClickable(false); |
| } |
| } |
| |
| /** |
| * @return the current transformation method for this TextView. |
| * This will frequently be null except for single-line and password |
| * fields. |
| */ |
| public final TransformationMethod getTransformationMethod() { |
| return mTransformation; |
| } |
| |
| /** |
| * Sets the transformation that is applied to the text that this |
| * TextView is displaying. |
| * |
| * @attr ref android.R.styleable#TextView_password |
| * @attr ref android.R.styleable#TextView_singleLine |
| */ |
| public final void setTransformationMethod(TransformationMethod method) { |
| if (method == mTransformation) { |
| // Avoid the setText() below if the transformation is |
| // the same. |
| return; |
| } |
| if (mTransformation != null) { |
| if (mText instanceof Spannable) { |
| ((Spannable) mText).removeSpan(mTransformation); |
| } |
| } |
| |
| mTransformation = method; |
| |
| setText(mText); |
| } |
| |
| /** |
| * Returns the top padding of the view, plus space for the top |
| * Drawable if any. |
| */ |
| public int getCompoundPaddingTop() { |
| final Drawables dr = mDrawables; |
| if (dr == null || dr.mDrawableTop == null) { |
| return mPaddingTop; |
| } else { |
| return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop; |
| } |
| } |
| |
| /** |
| * Returns the bottom padding of the view, plus space for the bottom |
| * Drawable if any. |
| */ |
| public int getCompoundPaddingBottom() { |
| final Drawables dr = mDrawables; |
| if (dr == null || dr.mDrawableBottom == null) { |
| return mPaddingBottom; |
| } else { |
| return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom; |
| } |
| } |
| |
| /** |
| * Returns the left padding of the view, plus space for the left |
| * Drawable if any. |
| */ |
| public int getCompoundPaddingLeft() { |
| final Drawables dr = mDrawables; |
| if (dr == null || dr.mDrawableLeft == null) { |
| return mPaddingLeft; |
| } else { |
| return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft; |
| } |
| } |
| |
| /** |
| * Returns the right padding of the view, plus space for the right |
| * Drawable if any. |
| */ |
| public int getCompoundPaddingRight() { |
| final Drawables dr = mDrawables; |
| if (dr == null || dr.mDrawableRight == null) { |
| return mPaddingRight; |
| } else { |
| return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight; |
| } |
| } |
| |
| /** |
| * Returns the extended top padding of the view, including both the |
| * top Drawable if any and any extra space to keep more than maxLines |
| * of text from showing. It is only valid to call this after measuring. |
| */ |
| public int getExtendedPaddingTop() { |
| if (mMaxMode != LINES) { |
| return getCompoundPaddingTop(); |
| } |
| |
| if (mLayout.getLineCount() <= mMaximum) { |
| return getCompoundPaddingTop(); |
| } |
| |
| int top = getCompoundPaddingTop(); |
| int bottom = getCompoundPaddingBottom(); |
| int viewht = getHeight() - top - bottom; |
| int layoutht = mLayout.getLineTop(mMaximum); |
| |
| if (layoutht >= viewht) { |
| return top; |
| } |
| |
| final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; |
| if (gravity == Gravity.TOP) { |
| return top; |
| } else if (gravity == Gravity.BOTTOM) { |
| return top + viewht - layoutht; |
| } else { // (gravity == Gravity.CENTER_VERTICAL) |
| return top + (viewht - layoutht) / 2; |
| } |
| } |
| |
| /** |
| * Returns the extended bottom padding of the view, including both the |
| * bottom Drawable if any and any extra space to keep more than maxLines |
| * of text from showing. It is only valid to call this after measuring. |
| */ |
| public int getExtendedPaddingBottom() { |
| if (mMaxMode != LINES) { |
| return getCompoundPaddingBottom(); |
| } |
| |
| if (mLayout.getLineCount() <= mMaximum) { |
| return getCompoundPaddingBottom(); |
| } |
| |
| int top = getCompoundPaddingTop(); |
| int bottom = getCompoundPaddingBottom(); |
| int viewht = getHeight() - top - bottom; |
| int layoutht = mLayout.getLineTop(mMaximum); |
| |
| if (layoutht >= viewht) { |
| return bottom; |
| } |
| |
| final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; |
| if (gravity == Gravity.TOP) { |
| return bottom + viewht - layoutht; |
| } else if (gravity == Gravity.BOTTOM) { |
| return bottom; |
| } else { // (gravity == Gravity.CENTER_VERTICAL) |
| return bottom + (viewht - layoutht) / 2; |
| } |
| } |
| |
| /** |
| * Returns the total left padding of the view, including the left |
| * Drawable if any. |
| */ |
| public int getTotalPaddingLeft() { |
| return getCompoundPaddingLeft(); |
| } |
| |
| /** |
| * Returns the total right padding of the view, including the right |
| * Drawable if any. |
| */ |
| public int getTotalPaddingRight() { |
| return getCompoundPaddingRight(); |
| } |
| |
| /** |
| * Returns the total top padding of the view, including the top |
| * Drawable if any, the extra space to keep more than maxLines |
| * from showing, and the vertical offset for gravity, if any. |
| */ |
| public int getTotalPaddingTop() { |
| return getExtendedPaddingTop() + getVerticalOffset(true); |
| } |
| |
| /** |
| * Returns the total bottom padding of the view, including the bottom |
| * Drawable if any, the extra space to keep more than maxLines |
| * from showing, and the vertical offset for gravity, if any. |
| */ |
| public int getTotalPaddingBottom() { |
| return getExtendedPaddingBottom() + getBottomVerticalOffset(true); |
| } |
| |
| /** |
| * Sets the Drawables (if any) to appear to the left of, above, |
| * to the right of, and below the text. Use null if you do not |
| * want a Drawable there. The Drawables must already have had |
| * {@link Drawable#setBounds} called. |
| * |
| * @attr ref android.R.styleable#TextView_drawableLeft |
| * @attr ref android.R.styleable#TextView_drawableTop |
| * @attr ref android.R.styleable#TextView_drawableRight |
| * @attr ref android.R.styleable#TextView_drawableBottom |
| */ |
| public void setCompoundDrawables(Drawable left, Drawable top, |
| Drawable right, Drawable bottom) { |
| Drawables dr = mDrawables; |
| |
| final boolean drawables = left != null || top != null |
| || right != null || bottom != null; |
| |
| if (!drawables) { |
| // Clearing drawables... can we free the data structure? |
| if (dr != null) { |
| if (dr.mDrawablePadding == 0) { |
| mDrawables = null; |
| } else { |
| // We need to retain the last set padding, so just clear |
| // out all of the fields in the existing structure. |
| if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); |
| dr.mDrawableLeft = null; |
| if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); |
| dr.mDrawableTop = null; |
| if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); |
| dr.mDrawableRight = null; |
| if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); |
| dr.mDrawableBottom = null; |
| dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; |
| dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; |
| dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; |
| dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; |
| } |
| } |
| } else { |
| if (dr == null) { |
| mDrawables = dr = new Drawables(); |
| } |
| |
| if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) { |
| dr.mDrawableLeft.setCallback(null); |
| } |
| dr.mDrawableLeft = left; |
| |
| if (dr.mDrawableTop != top && dr.mDrawableTop != null) { |
| dr.mDrawableTop.setCallback(null); |
| } |
| dr.mDrawableTop = top; |
| |
| if (dr.mDrawableRight != right && dr.mDrawableRight != null) { |
| dr.mDrawableRight.setCallback(null); |
| } |
| dr.mDrawableRight = right; |
| |
| if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { |
| dr.mDrawableBottom.setCallback(null); |
| } |
| dr.mDrawableBottom = bottom; |
| |
| final Rect compoundRect = dr.mCompoundRect; |
| int[] state; |
| |
| state = getDrawableState(); |
| |
| if (left != null) { |
| left.setState(state); |
| left.copyBounds(compoundRect); |
| left.setCallback(this); |
| dr.mDrawableSizeLeft = compoundRect.width(); |
| dr.mDrawableHeightLeft = compoundRect.height(); |
| } else { |
| dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; |
| } |
| |
| if (right != null) { |
| right.setState(state); |
| right.copyBounds(compoundRect); |
| right.setCallback(this); |
| dr.mDrawableSizeRight = compoundRect.width(); |
| dr.mDrawableHeightRight = compoundRect.height(); |
| } else { |
| dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; |
| } |
| |
| if (top != null) { |
| top.setState(state); |
| top.copyBounds(compoundRect); |
| top.setCallback(this); |
| dr.mDrawableSizeTop = compoundRect.height(); |
| dr.mDrawableWidthTop = compoundRect.width(); |
| } else { |
| dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; |
| } |
| |
| if (bottom != null) { |
| bottom.setState(state); |
| bottom.copyBounds(compoundRect); |
| bottom.setCallback(this); |
| dr.mDrawableSizeBottom = compoundRect.height(); |
| dr.mDrawableWidthBottom = compoundRect.width(); |
| } else { |
| dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; |
| } |
| } |
| |
| invalidate(); |
| requestLayout(); |
| } |
| |
| /** |
| * Sets the Drawables (if any) to appear to the left of, above, |
| * to the right of, and below the text. Use 0 if you do not |
| * want a Drawable there. The Drawables' bounds will be set to |
| * their intrinsic bounds. |
| * |
| * @param left Resource identifier of the left Drawable. |
| * @param top Resource identifier of the top Drawable. |
| * @param right Resource identifier of the right Drawable. |
| * @param bottom Resource identifier of the bottom Drawable. |
| * |
| * @attr ref android.R.styleable#TextView_drawableLeft |
| * @attr ref android.R.styleable#TextView_drawableTop |
| * @attr ref android.R.styleable#TextView_drawableRight |
| * @attr ref android.R.styleable#TextView_drawableBottom |
| */ |
| public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) { |
| final Resources resources = getContext().getResources(); |
| setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null, |
| top != 0 ? resources.getDrawable(top) : null, |
| right != 0 ? resources.getDrawable(right) : null, |
| bottom != 0 ? resources.getDrawable(bottom) : null); |
| } |
| |
| /** |
| * Sets the Drawables (if any) to appear to the left of, above, |
| * to the right of, and below the text. Use null if you do not |
| * want a Drawable there. The Drawables' bounds will be set to |
| * their intrinsic bounds. |
| * |
| * @attr ref android.R.styleable#TextView_drawableLeft |
| * @attr ref android.R.styleable#TextView_drawableTop |
| * @attr ref android.R.styleable#TextView_drawableRight |
| * @attr ref android.R.styleable#TextView_drawableBottom |
| */ |
| public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, |
| Drawable right, Drawable bottom) { |
| |
| if (left != null) { |
| left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight()); |
| } |
| if (right != null) { |
| right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight()); |
| } |
| if (top != null) { |
| top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight()); |
| } |
| if (bottom != null) { |
| bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); |
| } |
| setCompoundDrawables(left, top, right, bottom); |
| } |
| |
| /** |
| * Returns drawables for the left, top, right, and bottom borders. |
| */ |
| public Drawable[] getCompoundDrawables() { |
| final Drawables dr = mDrawables; |
| if (dr != null) { |
| return new Drawable[] { |
| dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom |
| }; |
| } else { |
| return new Drawable[] { null, null, null, null }; |
| } |
| } |
| |
| /** |
| * Sets the size of the padding between the compound drawables and |
| * the text. |
| * |
| * @attr ref android.R.styleable#TextView_drawablePadding |
| */ |
| public void setCompoundDrawablePadding(int pad) { |
| Drawables dr = mDrawables; |
| if (pad == 0) { |
| if (dr != null) { |
| dr.mDrawablePadding = pad; |
| } |
| } else { |
| if (dr == null) { |
| mDrawables = dr = new Drawables(); |
| } |
| dr.mDrawablePadding = pad; |
| } |
| |
| invalidate(); |
| requestLayout(); |
| } |
| |
| /** |
| * Returns the padding between the compound drawables and the text. |
| */ |
| public int getCompoundDrawablePadding() { |
| final Drawables dr = mDrawables; |
| return dr != null ? dr.mDrawablePadding : 0; |
| } |
| |
| @Override |
| public void setPadding(int left, int top, int right, int bottom) { |
| if (left != mPaddingLeft || |
| right != mPaddingRight || |
| top != mPaddingTop || |
| bottom != mPaddingBottom) { |
| nullLayouts(); |
| } |
| |
| // the super call will requestLayout() |
| super.setPadding(left, top, right, bottom); |
| invalidate(); |
| } |
| |
| /** |
| * Gets the autolink mask of the text. See {@link |
| * android.text.util.Linkify#ALL Linkify.ALL} and peers for |
| * possible values. |
| * |
| * @attr ref android.R.styleable#TextView_autoLink |
| */ |
| public final int getAutoLinkMask() { |
| return mAutoLinkMask; |
| } |
| |
| /** |
| * Sets the text color, size, style, hint color, and highlight color |
| * from the specified TextAppearance resource. |
| */ |
| public void setTextAppearance(Context context, int resid) { |
| TypedArray appearance = |
| context.obtainStyledAttributes(resid, |
| com.android.internal.R.styleable.TextAppearance); |
| |
| int color; |
| ColorStateList colors; |
| int ts; |
| |
| color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0); |
| if (color != 0) { |
| setHighlightColor(color); |
| } |
| |
| colors = appearance.getColorStateList(com.android.internal.R.styleable. |
| TextAppearance_textColor); |
| if (colors != null) { |
| setTextColor(colors); |
| } |
| |
| ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable. |
| TextAppearance_textSize, 0); |
| if (ts != 0) { |
| setRawTextSize(ts); |
| } |
| |
| colors = appearance.getColorStateList(com.android.internal.R.styleable. |
| TextAppearance_textColorHint); |
| if (colors != null) { |
| setHintTextColor(colors); |
| } |
| |
| colors = appearance.getColorStateList(com.android.internal.R.styleable. |
| TextAppearance_textColorLink); |
| if (colors != null) { |
| setLinkTextColor(colors); |
| } |
| |
| int typefaceIndex, styleIndex; |
| |
| typefaceIndex = appearance.getInt(com.android.internal.R.styleable. |
| TextAppearance_typeface, -1); |
| styleIndex = appearance.getInt(com.android.internal.R.styleable. |
| TextAppearance_textStyle, -1); |
| |
| setTypefaceByIndex(typefaceIndex, styleIndex); |
| appearance.recycle(); |
| } |
| |
| /** |
| * @return the size (in pixels) of the default text size in this TextView. |
| */ |
| public float getTextSize() { |
| return mTextPaint.getTextSize(); |
| } |
| |
| /** |
| * Set the default text size to the given value, interpreted as "scaled |
| * pixel" units. This size is adjusted based on the current density and |
| * user font size preference. |
| * |
| * @param size The scaled pixel size. |
| * |
| * @attr ref android.R.styleable#TextView_textSize |
| */ |
| @android.view.RemotableViewMethod |
| public void setTextSize(float size) { |
| setTextSize(TypedValue.COMPLEX_UNIT_SP, size); |
| } |
| |
| /** |
| * Set the default text size to a given unit and value. See {@link |
| * TypedValue} for the possible dimension units. |
| * |
| * @param unit The desired dimension unit. |
| * @param size The desired size in the given units. |
| * |
| * @attr ref android.R.styleable#TextView_textSize |
| */ |
| public void setTextSize(int unit, float size) { |
| Context c = getContext(); |
| Resources r; |
| |
| if (c == null) |
| r = Resources.getSystem(); |
| else |
| r = c.getResources(); |
| |
| setRawTextSize(TypedValue.applyDimension( |
| unit, size, r.getDisplayMetrics())); |
| } |
| |
| private void setRawTextSize(float size) { |
| if (size != mTextPaint.getTextSize()) { |
| mTextPaint.setTextSize(size); |
| |
| if (mLayout != null) { |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| } |
| |
| /** |
| * @return the extent by which text is currently being stretched |
| * horizontally. This will usually be 1. |
| */ |
| public float getTextScaleX() { |
| return mTextPaint.getTextScaleX(); |
| } |
| |
| /** |
| * Sets the extent by which text should be stretched horizontally. |
| * |
| * @attr ref android.R.styleable#TextView_textScaleX |
| */ |
| @android.view.RemotableViewMethod |
| public void setTextScaleX(float size) { |
| if (size != mTextPaint.getTextScaleX()) { |
| mUserSetTextScaleX = true; |
| mTextPaint.setTextScaleX(size); |
| |
| if (mLayout != null) { |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| } |
| |
| /** |
| * Sets the typeface and style in which the text should be displayed. |
| * Note that not all Typeface families actually have bold and italic |
| * variants, so you may need to use |
| * {@link #setTypeface(Typeface, int)} to get the appearance |
| * that you actually want. |
| * |
| * @attr ref android.R.styleable#TextView_typeface |
| * @attr ref android.R.styleable#TextView_textStyle |
| */ |
| public void setTypeface(Typeface tf) { |
| if (mTextPaint.getTypeface() != tf) { |
| mTextPaint.setTypeface(tf); |
| |
| if (mLayout != null) { |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| } |
| |
| /** |
| * @return the current typeface and style in which the text is being |
| * displayed. |
| */ |
| public Typeface getTypeface() { |
| return mTextPaint.getTypeface(); |
| } |
| |
| /** |
| * Sets the text color for all the states (normal, selected, |
| * focused) to be this color. |
| * |
| * @attr ref android.R.styleable#TextView_textColor |
| */ |
| @android.view.RemotableViewMethod |
| public void setTextColor(int color) { |
| mTextColor = ColorStateList.valueOf(color); |
| updateTextColors(); |
| } |
| |
| /** |
| * Sets the text color. |
| * |
| * @attr ref android.R.styleable#TextView_textColor |
| */ |
| public void setTextColor(ColorStateList colors) { |
| if (colors == null) { |
| throw new NullPointerException(); |
| } |
| |
| mTextColor = colors; |
| updateTextColors(); |
| } |
| |
| /** |
| * Return the set of text colors. |
| * |
| * @return Returns the set of text colors. |
| */ |
| public final ColorStateList getTextColors() { |
| return mTextColor; |
| } |
| |
| /** |
| * <p>Return the current color selected for normal text.</p> |
| * |
| * @return Returns the current text color. |
| */ |
| public final int getCurrentTextColor() { |
| return mCurTextColor; |
| } |
| |
| /** |
| * Sets the color used to display the selection highlight. |
| * |
| * @attr ref android.R.styleable#TextView_textColorHighlight |
| */ |
| @android.view.RemotableViewMethod |
| public void setHighlightColor(int color) { |
| if (mHighlightColor != color) { |
| mHighlightColor = color; |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Gives the text a shadow of the specified radius and color, the specified |
| * distance from its normal position. |
| * |
| * @attr ref android.R.styleable#TextView_shadowColor |
| * @attr ref android.R.styleable#TextView_shadowDx |
| * @attr ref android.R.styleable#TextView_shadowDy |
| * @attr ref android.R.styleable#TextView_shadowRadius |
| */ |
| public void setShadowLayer(float radius, float dx, float dy, int color) { |
| mTextPaint.setShadowLayer(radius, dx, dy, color); |
| |
| mShadowRadius = radius; |
| mShadowDx = dx; |
| mShadowDy = dy; |
| |
| invalidate(); |
| } |
| |
| /** |
| * @return the base paint used for the text. Please use this only to |
| * consult the Paint's properties and not to change them. |
| */ |
| public TextPaint getPaint() { |
| return mTextPaint; |
| } |
| |
| /** |
| * Sets the autolink mask of the text. See {@link |
| * android.text.util.Linkify#ALL Linkify.ALL} and peers for |
| * possible values. |
| * |
| * @attr ref android.R.styleable#TextView_autoLink |
| */ |
| @android.view.RemotableViewMethod |
| public final void setAutoLinkMask(int mask) { |
| mAutoLinkMask = mask; |
| } |
| |
| /** |
| * Sets whether the movement method will automatically be set to |
| * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been |
| * set to nonzero and links are detected in {@link #setText}. |
| * The default is true. |
| * |
| * @attr ref android.R.styleable#TextView_linksClickable |
| */ |
| @android.view.RemotableViewMethod |
| public final void setLinksClickable(boolean whether) { |
| mLinksClickable = whether; |
| } |
| |
| /** |
| * Returns whether the movement method will automatically be set to |
| * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been |
| * set to nonzero and links are detected in {@link #setText}. |
| * The default is true. |
| * |
| * @attr ref android.R.styleable#TextView_linksClickable |
| */ |
| public final boolean getLinksClickable() { |
| return mLinksClickable; |
| } |
| |
| /** |
| * Returns the list of URLSpans attached to the text |
| * (by {@link Linkify} or otherwise) if any. You can call |
| * {@link URLSpan#getURL} on them to find where they link to |
| * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd} |
| * to find the region of the text they are attached to. |
| */ |
| public URLSpan[] getUrls() { |
| if (mText instanceof Spanned) { |
| return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class); |
| } else { |
| return new URLSpan[0]; |
| } |
| } |
| |
| /** |
| * Sets the color of the hint text. |
| * |
| * @attr ref android.R.styleable#TextView_textColorHint |
| */ |
| @android.view.RemotableViewMethod |
| public final void setHintTextColor(int color) { |
| mHintTextColor = ColorStateList.valueOf(color); |
| updateTextColors(); |
| } |
| |
| /** |
| * Sets the color of the hint text. |
| * |
| * @attr ref android.R.styleable#TextView_textColorHint |
| */ |
| public final void setHintTextColor(ColorStateList colors) { |
| mHintTextColor = colors; |
| updateTextColors(); |
| } |
| |
| /** |
| * <p>Return the color used to paint the hint text.</p> |
| * |
| * @return Returns the list of hint text colors. |
| */ |
| public final ColorStateList getHintTextColors() { |
| return mHintTextColor; |
| } |
| |
| /** |
| * <p>Return the current color selected to paint the hint text.</p> |
| * |
| * @return Returns the current hint text color. |
| */ |
| public final int getCurrentHintTextColor() { |
| return mHintTextColor != null ? mCurHintTextColor : mCurTextColor; |
| } |
| |
| /** |
| * Sets the color of links in the text. |
| * |
| * @attr ref android.R.styleable#TextView_textColorLink |
| */ |
| @android.view.RemotableViewMethod |
| public final void setLinkTextColor(int color) { |
| mLinkTextColor = ColorStateList.valueOf(color); |
| updateTextColors(); |
| } |
| |
| /** |
| * Sets the color of links in the text. |
| * |
| * @attr ref android.R.styleable#TextView_textColorLink |
| */ |
| public final void setLinkTextColor(ColorStateList colors) { |
| mLinkTextColor = colors; |
| updateTextColors(); |
| } |
| |
| /** |
| * <p>Returns the color used to paint links in the text.</p> |
| * |
| * @return Returns the list of link text colors. |
| */ |
| public final ColorStateList getLinkTextColors() { |
| return mLinkTextColor; |
| } |
| |
| /** |
| * Sets the horizontal alignment of the text and the |
| * vertical gravity that will be used when there is extra space |
| * in the TextView beyond what is required for the text itself. |
| * |
| * @see android.view.Gravity |
| * @attr ref android.R.styleable#TextView_gravity |
| */ |
| public void setGravity(int gravity) { |
| if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { |
| gravity |= Gravity.LEFT; |
| } |
| if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { |
| gravity |= Gravity.TOP; |
| } |
| |
| boolean newLayout = false; |
| |
| if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != |
| (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) { |
| newLayout = true; |
| } |
| |
| if (gravity != mGravity) { |
| invalidate(); |
| } |
| |
| mGravity = gravity; |
| |
| if (mLayout != null && newLayout) { |
| // XXX this is heavy-handed because no actual content changes. |
| int want = mLayout.getWidth(); |
| int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth(); |
| |
| makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING, |
| mRight - mLeft - getCompoundPaddingLeft() - |
| getCompoundPaddingRight(), true); |
| } |
| } |
| |
| /** |
| * Returns the horizontal and vertical alignment of this TextView. |
| * |
| * @see android.view.Gravity |
| * @attr ref android.R.styleable#TextView_gravity |
| */ |
| public int getGravity() { |
| return mGravity; |
| } |
| |
| /** |
| * @return the flags on the Paint being used to display the text. |
| * @see Paint#getFlags |
| */ |
| public int getPaintFlags() { |
| return mTextPaint.getFlags(); |
| } |
| |
| /** |
| * Sets flags on the Paint being used to display the text and |
| * reflows the text if they are different from the old flags. |
| * @see Paint#setFlags |
| */ |
| @android.view.RemotableViewMethod |
| public void setPaintFlags(int flags) { |
| if (mTextPaint.getFlags() != flags) { |
| mTextPaint.setFlags(flags); |
| |
| if (mLayout != null) { |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| } |
| |
| /** |
| * Sets whether the text should be allowed to be wider than the |
| * View is. If false, it will be wrapped to the width of the View. |
| * |
| * @attr ref android.R.styleable#TextView_scrollHorizontally |
| */ |
| public void setHorizontallyScrolling(boolean whether) { |
| mHorizontallyScrolling = whether; |
| |
| if (mLayout != null) { |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Makes the TextView at least this many lines tall |
| * |
| * @attr ref android.R.styleable#TextView_minLines |
| */ |
| @android.view.RemotableViewMethod |
| public void setMinLines(int minlines) { |
| mMinimum = minlines; |
| mMinMode = LINES; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView at least this many pixels tall |
| * |
| * @attr ref android.R.styleable#TextView_minHeight |
| */ |
| @android.view.RemotableViewMethod |
| public void setMinHeight(int minHeight) { |
| mMinimum = minHeight; |
| mMinMode = PIXELS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView at most this many lines tall |
| * |
| * @attr ref android.R.styleable#TextView_maxLines |
| */ |
| @android.view.RemotableViewMethod |
| public void setMaxLines(int maxlines) { |
| mMaximum = maxlines; |
| mMaxMode = LINES; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView at most this many pixels tall |
| * |
| * @attr ref android.R.styleable#TextView_maxHeight |
| */ |
| @android.view.RemotableViewMethod |
| public void setMaxHeight(int maxHeight) { |
| mMaximum = maxHeight; |
| mMaxMode = PIXELS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView exactly this many lines tall |
| * |
| * @attr ref android.R.styleable#TextView_lines |
| */ |
| @android.view.RemotableViewMethod |
| public void setLines(int lines) { |
| mMaximum = mMinimum = lines; |
| mMaxMode = mMinMode = LINES; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView exactly this many pixels tall. |
| * You could do the same thing by specifying this number in the |
| * LayoutParams. |
| * |
| * @attr ref android.R.styleable#TextView_height |
| */ |
| @android.view.RemotableViewMethod |
| public void setHeight(int pixels) { |
| mMaximum = mMinimum = pixels; |
| mMaxMode = mMinMode = PIXELS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView at least this many ems wide |
| * |
| * @attr ref android.R.styleable#TextView_minEms |
| */ |
| @android.view.RemotableViewMethod |
| public void setMinEms(int minems) { |
| mMinWidth = minems; |
| mMinWidthMode = EMS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView at least this many pixels wide |
| * |
| * @attr ref android.R.styleable#TextView_minWidth |
| */ |
| @android.view.RemotableViewMethod |
| public void setMinWidth(int minpixels) { |
| mMinWidth = minpixels; |
| mMinWidthMode = PIXELS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView at most this many ems wide |
| * |
| * @attr ref android.R.styleable#TextView_maxEms |
| */ |
| @android.view.RemotableViewMethod |
| public void setMaxEms(int maxems) { |
| mMaxWidth = maxems; |
| mMaxWidthMode = EMS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView at most this many pixels wide |
| * |
| * @attr ref android.R.styleable#TextView_maxWidth |
| */ |
| @android.view.RemotableViewMethod |
| public void setMaxWidth(int maxpixels) { |
| mMaxWidth = maxpixels; |
| mMaxWidthMode = PIXELS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView exactly this many ems wide |
| * |
| * @attr ref android.R.styleable#TextView_ems |
| */ |
| @android.view.RemotableViewMethod |
| public void setEms(int ems) { |
| mMaxWidth = mMinWidth = ems; |
| mMaxWidthMode = mMinWidthMode = EMS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| /** |
| * Makes the TextView exactly this many pixels wide. |
| * You could do the same thing by specifying this number in the |
| * LayoutParams. |
| * |
| * @attr ref android.R.styleable#TextView_width |
| */ |
| @android.view.RemotableViewMethod |
| public void setWidth(int pixels) { |
| mMaxWidth = mMinWidth = pixels; |
| mMaxWidthMode = mMinWidthMode = PIXELS; |
| |
| requestLayout(); |
| invalidate(); |
| } |
| |
| |
| /** |
| * Sets line spacing for this TextView. Each line will have its height |
| * multiplied by <code>mult</code> and have <code>add</code> added to it. |
| * |
| * @attr ref android.R.styleable#TextView_lineSpacingExtra |
| * @attr ref android.R.styleable#TextView_lineSpacingMultiplier |
| */ |
| public void setLineSpacing(float add, float mult) { |
| mSpacingMult = mult; |
| mSpacingAdd = add; |
| |
| if (mLayout != null) { |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Convenience method: Append the specified text to the TextView's |
| * display buffer, upgrading it to BufferType.EDITABLE if it was |
| * not already editable. |
| */ |
| public final void append(CharSequence text) { |
| append(text, 0, text.length()); |
| } |
| |
| /** |
| * Convenience method: Append the specified text slice to the TextView's |
| * display buffer, upgrading it to BufferType.EDITABLE if it was |
| * not already editable. |
| */ |
| public void append(CharSequence text, int start, int end) { |
| if (!(mText instanceof Editable)) { |
| setText(mText, BufferType.EDITABLE); |
| } |
| |
| ((Editable) mText).append(text, start, end); |
| } |
| |
| private void updateTextColors() { |
| boolean inval = false; |
| int color = mTextColor.getColorForState(getDrawableState(), 0); |
| if (color != mCurTextColor) { |
| mCurTextColor = color; |
| inval = true; |
| } |
| if (mLinkTextColor != null) { |
| color = mLinkTextColor.getColorForState(getDrawableState(), 0); |
| if (color != mTextPaint.linkColor) { |
| mTextPaint.linkColor = color; |
| inval = true; |
| } |
| } |
| if (mHintTextColor != null) { |
| color = mHintTextColor.getColorForState(getDrawableState(), 0); |
| if (color != mCurHintTextColor && mText.length() == 0) { |
| mCurHintTextColor = color; |
| inval = true; |
| } |
| } |
| if (inval) { |
| invalidate(); |
| } |
| } |
| |
| @Override |
| protected void drawableStateChanged() { |
| super.drawableStateChanged(); |
| if (mTextColor != null && mTextColor.isStateful() |
| || (mHintTextColor != null && mHintTextColor.isStateful()) |
| || (mLinkTextColor != null && mLinkTextColor.isStateful())) { |
| updateTextColors(); |
| } |
| |
| final Drawables dr = mDrawables; |
| if (dr != null) { |
| int[] state = getDrawableState(); |
| if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) { |
| dr.mDrawableTop.setState(state); |
| } |
| if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) { |
| dr.mDrawableBottom.setState(state); |
| } |
| if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) { |
| dr.mDrawableLeft.setState(state); |
| } |
| if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) { |
| dr.mDrawableRight.setState(state); |
| } |
| } |
| } |
| |
| /** |
| * User interface state that is stored by TextView for implementing |
| * {@link View#onSaveInstanceState}. |
| */ |
| public static class SavedState extends BaseSavedState { |
| int selStart; |
| int selEnd; |
| CharSequence text; |
| boolean frozenWithFocus; |
| CharSequence error; |
| |
| SavedState(Parcelable superState) { |
| super(superState); |
| } |
| |
| @Override |
| public void writeToParcel(Parcel out, int flags) { |
| super.writeToParcel(out, flags); |
| out.writeInt(selStart); |
| out.writeInt(selEnd); |
| out.writeInt(frozenWithFocus ? 1 : 0); |
| TextUtils.writeToParcel(text, out, flags); |
| |
| if (error == null) { |
| out.writeInt(0); |
| } else { |
| out.writeInt(1); |
| TextUtils.writeToParcel(error, out, flags); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| String str = "TextView.SavedState{" |
| + Integer.toHexString(System.identityHashCode(this)) |
| + " start=" + selStart + " end=" + selEnd; |
| if (text != null) { |
| str += " text=" + text; |
| } |
| return str + "}"; |
| } |
| |
| @SuppressWarnings("hiding") |
| public static final Parcelable.Creator<SavedState> CREATOR |
| = new Parcelable.Creator<SavedState>() { |
| public SavedState createFromParcel(Parcel in) { |
| return new SavedState(in); |
| } |
| |
| public SavedState[] newArray(int size) { |
| return new SavedState[size]; |
| } |
| }; |
| |
| private SavedState(Parcel in) { |
| super(in); |
| selStart = in.readInt(); |
| selEnd = in.readInt(); |
| frozenWithFocus = (in.readInt() != 0); |
| text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| |
| if (in.readInt() != 0) { |
| error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| } |
| } |
| } |
| |
| @Override |
| public Parcelable onSaveInstanceState() { |
| Parcelable superState = super.onSaveInstanceState(); |
| |
| // Save state if we are forced to |
| boolean save = mFreezesText; |
| int start = 0; |
| int end = 0; |
| |
| if (mText != null) { |
| start = 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); |
| } |
| |
| ss.text = sp; |
| } else { |
| ss.text = mText.toString(); |
| } |
| |
| if (isFocused() && start >= 0 && end >= 0) { |
| ss.frozenWithFocus = true; |
| } |
| |
| ss.error = mError; |
| |
| return ss; |
| } |
| |
| return superState; |
| } |
| |
| @Override |
| public void onRestoreInstanceState(Parcelable state) { |
| if (!(state instanceof SavedState)) { |
| super.onRestoreInstanceState(state); |
| return; |
| } |
| |
| SavedState ss = (SavedState)state; |
| super.onRestoreInstanceState(ss.getSuperState()); |
| |
| // XXX restore buffer type too, as well as lots of other stuff |
| if (ss.text != null) { |
| setText(ss.text); |
| } |
| |
| if (ss.selStart >= 0 && ss.selEnd >= 0) { |
| if (mText instanceof Spannable) { |
| int len = mText.length(); |
| |
| if (ss.selStart > len || ss.selEnd > len) { |
| String restored = ""; |
| |
| if (ss.text != null) { |
| restored = "(restored) "; |
| } |
| |
| Log.e(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) { |
| 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 <em>does not</em> accept |
| * HTML-like formatting, which you can do with text strings in XML resource files. |
| * To style your strings, attach android.text.style.* objects to a |
| * {@link android.text.SpannableString SpannableString}, or see the |
| * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources"> |
| * Available Resource Types</a> documentation for an example of setting |
| * formatted text in the XML resource file. |
| * |
| * @attr ref android.R.styleable#TextView_text |
| */ |
| @android.view.RemotableViewMethod |
| public final void setText(CharSequence text) { |
| setText(text, mBufferType); |
| } |
| |
| /** |
| * Like {@link #setText(CharSequence)}, |
| * except that the cursor position (if any) is retained in the new text. |
| * |
| * @param text The new text to place in the text view. |
| * |
| * @see #setText(CharSequence) |
| */ |
| @android.view.RemotableViewMethod |
| public final void setTextKeepState(CharSequence text) { |
| setTextKeepState(text, mBufferType); |
| } |
| |
| /** |
| * Sets the text that this TextView is to display (see |
| * {@link #setText(CharSequence)}) and also sets whether it is stored |
| * in a styleable/spannable buffer and whether it is editable. |
| * |
| * @attr ref android.R.styleable#TextView_text |
| * @attr ref android.R.styleable#TextView_bufferType |
| */ |
| public void setText(CharSequence text, BufferType type) { |
| setText(text, type, true, 0); |
| |
| if (mCharWrapper != null) { |
| mCharWrapper.mChars = null; |
| } |
| } |
| |
| private void setText(CharSequence text, BufferType type, |
| boolean notifyBefore, int oldlen) { |
| if (text == null) { |
| text = ""; |
| } |
| |
| if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f); |
| |
| if (text instanceof Spanned && |
| ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) { |
| setHorizontalFadingEdgeEnabled(true); |
| setEllipsize(TextUtils.TruncateAt.MARQUEE); |
| } |
| |
| int n = mFilters.length; |
| for (int i = 0; i < n; i++) { |
| CharSequence out = mFilters[i].filter(text, 0, text.length(), |
| EMPTY_SPANNED, 0, 0); |
| if (out != null) { |
| text = out; |
| } |
| } |
| |
| if (notifyBefore) { |
| if (mText != null) { |
| oldlen = mText.length(); |
| sendBeforeTextChanged(mText, 0, oldlen, text.length()); |
| } else { |
| sendBeforeTextChanged("", 0, 0, text.length()); |
| } |
| } |
| |
| boolean needEditableForNotification = false; |
| |
| if (mListeners != null && mListeners.size() != 0) { |
| needEditableForNotification = true; |
| } |
| |
| if (type == BufferType.EDITABLE || mInput != null || |
| needEditableForNotification) { |
| Editable t = mEditableFactory.newEditable(text); |
| text = t; |
| setFilters(t, mFilters); |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| if (imm != null) imm.restartInput(this); |
| } else if (type == BufferType.SPANNABLE || mMovement != null) { |
| text = mSpannableFactory.newSpannable(text); |
| } else if (!(text instanceof CharWrapper)) { |
| text = TextUtils.stringOrSpannedString(text); |
| } |
| |
| if (mAutoLinkMask != 0) { |
| Spannable s2; |
| |
| if (type == BufferType.EDITABLE || text instanceof Spannable) { |
| s2 = (Spannable) text; |
| } else { |
| s2 = mSpannableFactory.newSpannable(text); |
| } |
| |
| if (Linkify.addLinks(s2, mAutoLinkMask)) { |
| text = s2; |
| type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE; |
| |
| /* |
| * We must go ahead and set the text before changing the |
| * movement method, because setMovementMethod() may call |
| * setText() again to try to upgrade the buffer type. |
| */ |
| mText = text; |
| |
| if (mLinksClickable) { |
| setMovementMethod(LinkMovementMethod.getInstance()); |
| } |
| } |
| } |
| |
| mBufferType = type; |
| mText = text; |
| |
| if (mTransformation == null) |
| mTransformed = text; |
| else |
| mTransformed = mTransformation.getTransformation(text, this); |
| |
| final int textLength = text.length(); |
| |
| if (text instanceof Spannable) { |
| Spannable sp = (Spannable) text; |
| |
| // Remove any ChangeWatchers that might have come |
| // from other TextViews. |
| final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class); |
| final int count = watchers.length; |
| for (int i = 0; i < count; i++) |
| sp.removeSpan(watchers[i]); |
| |
| if (mChangeWatcher == null) |
| mChangeWatcher = new ChangeWatcher(); |
| |
| sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE | |
| (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT)); |
| |
| if (mInput != null) { |
| sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); |
| } |
| |
| if (mTransformation != null) { |
| sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); |
| |
| } |
| |
| if (mMovement != null) { |
| mMovement.initialize(this, (Spannable) text); |
| |
| /* |
| * Initializing the movement method will have set the |
| * selection, so reset mSelectionMoved to keep that from |
| * interfering with the normal on-focus selection-setting. |
| */ |
| mSelectionMoved = false; |
| } |
| } |
| |
| if (mLayout != null) { |
| checkForRelayout(); |
| } |
| |
| sendOnTextChanged(text, 0, oldlen, textLength); |
| onTextChanged(text, 0, oldlen, textLength); |
| |
| if (needEditableForNotification) { |
| sendAfterTextChanged((Editable) text); |
| } |
| |
| // SelectionModifierCursorController depends on canSelectText, which depends on text |
| 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); |
| } |
| |
| private static class CharWrapper |
| implements CharSequence, GetChars, GraphicsOperations { |
| private char[] mChars; |
| private int mStart, mLength; |
| |
| public CharWrapper(char[] chars, int start, int len) { |
| mChars = chars; |
| mStart = start; |
| mLength = len; |
| } |
| |
| /* package */ void set(char[] chars, int start, int len) { |
| mChars = chars; |
| mStart = start; |
| mLength = len; |
| } |
| |
| public int length() { |
| return mLength; |
| } |
| |
| public char charAt(int off) { |
| return mChars[off + mStart]; |
| } |
| |
| @Override |
| public String toString() { |
| return new String(mChars, mStart, mLength); |
| } |
| |
| public CharSequence subSequence(int start, int end) { |
| if (start < 0 || end < 0 || start > mLength || end > mLength) { |
| throw new IndexOutOfBoundsException(start + ", " + end); |
| } |
| |
| return new String(mChars, start + mStart, end - start); |
| } |
| |
| public void getChars(int start, int end, char[] buf, int off) { |
| if (start < 0 || end < 0 || start > mLength || end > mLength) { |
| throw new IndexOutOfBoundsException(start + ", " + end); |
| } |
| |
| System.arraycopy(mChars, start + mStart, buf, off, end - start); |
| } |
| |
| public void drawText(Canvas c, int start, int end, |
| float x, float y, Paint p) { |
| c.drawText(mChars, start + mStart, end - start, x, y, p); |
| } |
| |
| public float measureText(int start, int end, Paint p) { |
| return p.measureText(mChars, start + mStart, end - start); |
| } |
| |
| public int getTextWidths(int start, int end, float[] widths, Paint p) { |
| return p.getTextWidths(mChars, start + mStart, end - start, widths); |
| } |
| } |
| |
| /** |
| * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)}, |
| * except that the cursor position (if any) is retained in the new text. |
| * |
| * @see #setText(CharSequence, android.widget.TextView.BufferType) |
| */ |
| public final void setTextKeepState(CharSequence text, BufferType type) { |
| int start = getSelectionStart(); |
| int end = getSelectionEnd(); |
| int len = text.length(); |
| |
| setText(text, type); |
| |
| if (start >= 0 || end >= 0) { |
| if (mText instanceof Spannable) { |
| Selection.setSelection((Spannable) mText, |
| Math.max(0, Math.min(start, len)), |
| Math.max(0, Math.min(end, len))); |
| } |
| } |
| } |
| |
| @android.view.RemotableViewMethod |
| public final void setText(int resid) { |
| setText(getContext().getResources().getText(resid)); |
| } |
| |
| public final void setText(int resid, BufferType type) { |
| setText(getContext().getResources().getText(resid), type); |
| } |
| |
| /** |
| * Sets the text to be displayed when the text of the TextView is empty. |
| * Null means to use the normal empty text. The hint does not currently |
| * participate in determining the size of the view. |
| * |
| * @attr ref android.R.styleable#TextView_hint |
| */ |
| @android.view.RemotableViewMethod |
| public final void setHint(CharSequence hint) { |
| mHint = TextUtils.stringOrSpannedString(hint); |
| |
| if (mLayout != null) { |
| checkForRelayout(); |
| } |
| |
| if (mText.length() == 0) { |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Sets the text to be displayed when the text of the TextView is empty, |
| * from a resource. |
| * |
| * @attr ref android.R.styleable#TextView_hint |
| */ |
| @android.view.RemotableViewMethod |
| public final void setHint(int resid) { |
| setHint(getContext().getResources().getText(resid)); |
| } |
| |
| /** |
| * Returns the hint that is displayed when the text of the TextView |
| * is empty. |
| * |
| * @attr ref android.R.styleable#TextView_hint |
| */ |
| @ViewDebug.CapturedViewProperty |
| public CharSequence getHint() { |
| return mHint; |
| } |
| |
| /** |
| * Set the type of the content with a constant as defined for |
| * {@link EditorInfo#inputType}. This will take care of changing |
| * the key listener, by calling {@link #setKeyListener(KeyListener)}, to |
| * match the given content type. If the given content type is |
| * {@link EditorInfo#TYPE_NULL} then a soft keyboard will |
| * not be displayed for this text view. |
| * |
| * @see #getInputType() |
| * @see #setRawInputType(int) |
| * @see android.text.InputType |
| * @attr ref android.R.styleable#TextView_inputType |
| */ |
| public void setInputType(int type) { |
| final boolean wasPassword = isPasswordInputType(mInputType); |
| final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType); |
| 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 multiLine = (type&(EditorInfo.TYPE_MASK_CLASS |
| | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) == |
| (EditorInfo.TYPE_CLASS_TEXT |
| | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); |
| |
| // We need to update the single line mode if it has changed or we |
| // were previously in password mode. |
| if (mSingleLine == multiLine || forceUpdate) { |
| // Change single line mode, but only change the transformation if |
| // we are not in password mode. |
| applySingleLine(!multiLine, !isPassword); |
| } |
| |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| if (imm != null) imm.restartInput(this); |
| } |
| |
| private 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); |
| } |
| |
| private 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) { |
| 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) mInput = input; |
| else { |
| setKeyListenerOnly(input); |
| } |
| } |
| |
| /** |
| * Get the type of the content. |
| * |
| * @see #setInputType(int) |
| * @see android.text.InputType |
| */ |
| public int getInputType() { |
| return mInputType; |
| } |
| |
| /** |
| * Change the editor type integer associated with the text view, which |
| * will be reported to an IME with {@link EditorInfo#imeOptions} when it |
| * has focus. |
| * @see #getImeOptions |
| * @see android.view.inputmethod.EditorInfo |
| * @attr ref android.R.styleable#TextView_imeOptions |
| */ |
| public void setImeOptions(int imeOptions) { |
| if (mInputContentType == null) { |
| mInputContentType = new InputContentType(); |
| } |
| mInputContentType.imeOptions = imeOptions; |
| } |
| |
| /** |
| * Get the type of the IME editor. |
| * |
| * @see #setImeOptions(int) |
| * @see android.view.inputmethod.EditorInfo |
| */ |
| public int getImeOptions() { |
| return mInputContentType != null |
| ? mInputContentType.imeOptions : EditorInfo.IME_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) { |
| if (mInputContentType == null) { |
| mInputContentType = new InputContentType(); |
| } |
| mInputContentType.imeActionLabel = label; |
| mInputContentType.imeActionId = actionId; |
| } |
| |
| /** |
| * Get the IME action label previous set with {@link #setImeActionLabel}. |
| * |
| * @see #setImeActionLabel |
| * @see android.view.inputmethod.EditorInfo |
| */ |
| public CharSequence getImeActionLabel() { |
| return mInputContentType != null |
| ? mInputContentType.imeActionLabel : null; |
| } |
| |
| /** |
| * Get the IME action ID previous set with {@link #setImeActionLabel}. |
| * |
| * @see #setImeActionLabel |
| * @see android.view.inputmethod.EditorInfo |
| */ |
| public int getImeActionId() { |
| return mInputContentType != null |
| ? mInputContentType.imeActionId : 0; |
| } |
| |
| /** |
| * Set a special listener to be called when an action is performed |
| * on the text view. This will be called when the enter key is pressed, |
| * or when an action supplied to the IME is selected by the user. Setting |
| * this means that the normal hard key event will not insert a newline |
| * into the text view, even if it is multi-line; holding down the ALT |
| * modifier will, however, allow the user to insert a newline character. |
| */ |
| public void setOnEditorActionListener(OnEditorActionListener l) { |
| if (mInputContentType == null) { |
| mInputContentType = new InputContentType(); |
| } |
| mInputContentType.onEditorActionListener = l; |
| } |
| |
| /** |
| * Called when an attached input method calls |
| * {@link InputConnection#performEditorAction(int) |
| * InputConnection.performEditorAction()} |
| * for this text view. The default implementation will call your action |
| * listener supplied to {@link #setOnEditorActionListener}, or perform |
| * a standard operation for {@link EditorInfo#IME_ACTION_NEXT |
| * EditorInfo.IME_ACTION_NEXT} or {@link EditorInfo#IME_ACTION_DONE |
| * EditorInfo.IME_ACTION_DONE}. |
| * |
| * <p>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 InputContentType ict = mInputContentType; |
| if (ict != null) { |
| if (ict.onEditorActionListener != null) { |
| if (ict.onEditorActionListener.onEditorAction(this, |
| actionCode, null)) { |
| return; |
| } |
| } |
| |
| // This is the handling for some default action. |
| // Note that for backwards compatibility we don't do this |
| // default handling if explicit ime options have not been given, |
| // instead turning this into the normal enter key codes that an |
| // app may be expecting. |
| if (actionCode == EditorInfo.IME_ACTION_NEXT) { |
| View v = focusSearch(FOCUS_DOWN); |
| if (v != null) { |
| if (!v.requestFocus(FOCUS_DOWN)) { |
| throw new IllegalStateException("focus search returned a view " + |
| "that wasn't able to take focus!"); |
| } |
| } |
| return; |
| |
| } else if (actionCode == EditorInfo.IME_ACTION_DONE) { |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| if (imm != null) { |
| imm.hideSoftInputFromWindow(getWindowToken(), 0); |
| } |
| return; |
| } |
| } |
| |
| Handler h = getHandler(); |
| if (h != null) { |
| long eventTime = SystemClock.uptimeMillis(); |
| h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, |
| new KeyEvent(eventTime, eventTime, |
| KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, |
| KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE |
| | KeyEvent.FLAG_EDITOR_ACTION))); |
| h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, |
| new KeyEvent(SystemClock.uptimeMillis(), eventTime, |
| KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, |
| KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE |
| | KeyEvent.FLAG_EDITOR_ACTION))); |
| } |
| } |
| |
| /** |
| * Set the private content type of the text, which is the |
| * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions} |
| * field that will be filled in when creating an input connection. |
| * |
| * @see #getPrivateImeOptions() |
| * @see EditorInfo#privateImeOptions |
| * @attr ref android.R.styleable#TextView_privateImeOptions |
| */ |
| public void setPrivateImeOptions(String type) { |
| if (mInputContentType == null) mInputContentType = new InputContentType(); |
| mInputContentType.privateImeOptions = type; |
| } |
| |
| /** |
| * Get the private type of the content. |
| * |
| * @see #setPrivateImeOptions(String) |
| * @see EditorInfo#privateImeOptions |
| */ |
| public String getPrivateImeOptions() { |
| return mInputContentType != null |
| ? mInputContentType.privateImeOptions : null; |
| } |
| |
| /** |
| * Set the extra input data of the text, which is the |
| * {@link EditorInfo#extras TextBoxAttribute.extras} |
| * Bundle that will be filled in when creating an input connection. The |
| * given integer is the resource ID of an XML resource holding an |
| * {@link android.R.styleable#InputExtras <input-extras>} XML tree. |
| * |
| * @see #getInputExtras(boolean) |
| * @see EditorInfo#extras |
| * @attr ref android.R.styleable#TextView_editorExtras |
| */ |
| public void setInputExtras(int xmlResId) |
| throws XmlPullParserException, IOException { |
| XmlResourceParser parser = getResources().getXml(xmlResId); |
| if (mInputContentType == null) mInputContentType = new InputContentType(); |
| mInputContentType.extras = new Bundle(); |
| getResources().parseBundleExtras(parser, mInputContentType.extras); |
| } |
| |
| /** |
| * Retrieve the input extras currently associated with the text view, which |
| * can be viewed as well as modified. |
| * |
| * @param create If true, the extras will be created if they don't already |
| * exist. Otherwise, null will be returned if none have been created. |
| * @see #setInputExtras(int) |
| * @see EditorInfo#extras |
| * @attr ref android.R.styleable#TextView_editorExtras |
| */ |
| public Bundle getInputExtras(boolean create) { |
| if (mInputContentType == null) { |
| if (!create) return null; |
| mInputContentType = new InputContentType(); |
| } |
| if (mInputContentType.extras == null) { |
| if (!create) return null; |
| mInputContentType.extras = new Bundle(); |
| } |
| return mInputContentType.extras; |
| } |
| |
| /** |
| * Returns the error message that was set to be displayed with |
| * {@link #setError}, or <code>null</code> if no error was set |
| * or if it the error was cleared by the widget after user input. |
| */ |
| public CharSequence getError() { |
| return mError; |
| } |
| |
| /** |
| * Sets the right-hand compound drawable of the TextView to the "error" |
| * icon and sets an error message that will be displayed in a popup when |
| * the TextView has focus. The icon and error message will be reset to |
| * null when any key events cause changes to the TextView's text. If the |
| * <code>error</code> is <code>null</code>, the error message and icon |
| * will be cleared. |
| */ |
| @android.view.RemotableViewMethod |
| public void setError(CharSequence error) { |
| if (error == null) { |
| setError(null, null); |
| } else { |
| Drawable dr = getContext().getResources(). |
| getDrawable(com.android.internal.R.drawable. |
| indicator_input_error); |
| |
| dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight()); |
| setError(error, dr); |
| } |
| } |
| |
| /** |
| * Sets the right-hand compound drawable of the TextView to the specified |
| * icon and sets an error message that will be displayed in a popup when |
| * the TextView has focus. The icon and error message will be reset to |
| * null when any key events cause changes to the TextView's text. The |
| * drawable must already have had {@link Drawable#setBounds} set on it. |
| * If the <code>error</code> is <code>null</code>, the error message will |
| * be cleared (and you should provide a <code>null</code> icon as well). |
| */ |
| public void setError(CharSequence error, Drawable icon) { |
| error = TextUtils.stringOrSpannedString(error); |
| |
| mError = error; |
| mErrorWasChanged = true; |
| final Drawables dr = mDrawables; |
| if (dr != null) { |
| setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, |
| icon, dr.mDrawableBottom); |
| } else { |
| setCompoundDrawables(null, null, icon, null); |
| } |
| |
| if (error == null) { |
| if (mPopup != null) { |
| if (mPopup.isShowing()) { |
| mPopup.dismiss(); |
| } |
| |
| mPopup = null; |
| } |
| } else { |
| if (isFocused()) { |
| showError(); |
| } |
| } |
| } |
| |
| private void showError() { |
| if (getWindowToken() == null) { |
| mShowErrorAfterAttach = true; |
| return; |
| } |
| |
| if (mPopup == null) { |
| LayoutInflater inflater = LayoutInflater.from(getContext()); |
| final TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint, |
| null); |
| |
| final float scale = getResources().getDisplayMetrics().density; |
| mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), |
| (int) (50 * scale + 0.5f)); |
| mPopup.setFocusable(false); |
| // The user is entering text, so the input method is needed. We |
| // don't want the popup to be displayed on top of it. |
| mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); |
| } |
| |
| TextView tv = (TextView) mPopup.getContentView(); |
| chooseSize(mPopup, mError, tv); |
| tv.setText(mError); |
| |
| mPopup.showAsDropDown(this, getErrorX(), getErrorY()); |
| mPopup.fixDirection(mPopup.isAboveAnchor()); |
| } |
| |
| private static class ErrorPopup extends PopupWindow { |
| private boolean mAbove = false; |
| private final TextView mView; |
| |
| ErrorPopup(TextView v, int width, int height) { |
| super(v, width, height); |
| mView = v; |
| } |
| |
| void fixDirection(boolean above) { |
| mAbove = above; |
| |
| if (above) { |
| mView.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error_above); |
| } else { |
| mView.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error); |
| } |
| } |
| |
| @Override |
| public void update(int x, int y, int w, int h, boolean force) { |
| super.update(x, y, w, h, force); |
| |
| boolean above = isAboveAnchor(); |
| if (above != mAbove) { |
| fixDirection(above); |
| } |
| } |
| } |
| |
| /** |
| * Returns the Y offset to make the pointy top of the error point |
| * at the middle of the error icon. |
| */ |
| private int getErrorX() { |
| /* |
| * The "25" is the distance between the point and the right edge |
| * of the background |
| */ |
| final float scale = getResources().getDisplayMetrics().density; |
| |
| final Drawables dr = mDrawables; |
| return getWidth() - mPopup.getWidth() |
| - getPaddingRight() |
| - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f); |
| } |
| |
| /** |
| * Returns the Y offset to make the pointy top of the error point |
| * at the bottom of the error icon. |
| */ |
| private int getErrorY() { |
| /* |
| * Compound, not extended, because the icon is not clipped |
| * if the text height is smaller. |
| */ |
| int vspace = mBottom - mTop - |
| getCompoundPaddingBottom() - getCompoundPaddingTop(); |
| |
| final Drawables dr = mDrawables; |
| int icontop = getCompoundPaddingTop() |
| + (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2; |
| |
| /* |
| * The "2" is the distance between the point and the top edge |
| * of the background. |
| */ |
| |
| return icontop + (dr != null ? dr.mDrawableHeightRight : 0) |
| - getHeight() - 2; |
| } |
| |
| private void hideError() { |
| if (mPopup != null) { |
| if (mPopup.isShowing()) { |
| mPopup.dismiss(); |
| } |
| } |
| |
| mShowErrorAfterAttach = false; |
| } |
| |
| private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { |
| int wid = tv.getPaddingLeft() + tv.getPaddingRight(); |
| int ht = tv.getPaddingTop() + tv.getPaddingBottom(); |
| |
| /* |
| * Figure out how big the text would be if we laid it out to the |
| * full width of this view minus the border. |
| */ |
| int cap = getWidth() - wid; |
| if (cap < 0) { |
| cap = 200; // We must not be measured yet -- setFrame() will fix it. |
| } |
| |
| Layout l = new StaticLayout(text, tv.getPaint(), cap, |
| Layout.Alignment.ALIGN_NORMAL, 1, 0, true); |
| float max = 0; |
| for (int i = 0; i < l.getLineCount(); i++) { |
| max = Math.max(max, l.getLineWidth(i)); |
| } |
| |
| /* |
| * Now set the popup size to be big enough for the text plus the border. |
| */ |
| pop.setWidth(wid + (int) Math.ceil(max)); |
| pop.setHeight(ht + l.getHeight()); |
| } |
| |
| |
| @Override |
| protected boolean setFrame(int l, int t, int r, int b) { |
| boolean result = super.setFrame(l, t, r, b); |
| |
| if (mPopup != null) { |
| TextView tv = (TextView) mPopup.getContentView(); |
| chooseSize(mPopup, mError, tv); |
| mPopup.update(this, getErrorX(), getErrorY(), |
| mPopup.getWidth(), mPopup.getHeight()); |
| } |
| |
| restartMarqueeIfNeeded(); |
| |
| return result; |
| } |
| |
| private void restartMarqueeIfNeeded() { |
| if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) { |
| mRestartMarquee = false; |
| startMarquee(); |
| } |
| } |
| |
| /** |
| * Sets the list of input filters that will be used if the buffer is |
| * Editable. Has no effect otherwise. |
| * |
| * @attr ref android.R.styleable#TextView_maxLength |
| */ |
| public void setFilters(InputFilter[] filters) { |
| if (filters == null) { |
| throw new IllegalArgumentException(); |
| } |
| |
| mFilters = filters; |
| |
| if (mText instanceof Editable) { |
| setFilters((Editable) mText, filters); |
| } |
| } |
| |
| /** |
| * Sets the list of input filters on the specified Editable, |
| * and includes mInput in the list if it is an InputFilter. |
| */ |
| private void setFilters(Editable e, InputFilter[] filters) { |
| if (mInput instanceof InputFilter) { |
| InputFilter[] nf = new InputFilter[filters.length + 1]; |
| |
| System.arraycopy(filters, 0, nf, 0, filters.length); |
| nf[filters.length] = (InputFilter) mInput; |
| |
| e.setFilters(nf); |
| } else { |
| e.setFilters(filters); |
| } |
| } |
| |
| /** |
| * Returns the current list of input filters. |
| */ |
| public InputFilter[] getFilters() { |
| return mFilters; |
| } |
| |
| ///////////////////////////////////////////////////////////////////////// |
| |
| private int getVerticalOffset(boolean forceNormal) { |
| int voffset = 0; |
| final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; |
| |
| Layout l = mLayout; |
| if (!forceNormal && mText.length() == 0 && mHintLayout != null) { |
| l = mHintLayout; |
| } |
| |
| if (gravity != Gravity.TOP) { |
| int boxht; |
| |
| if (l == mHintLayout) { |
| boxht = getMeasuredHeight() - getCompoundPaddingTop() - |
| getCompoundPaddingBottom(); |
| } else { |
| boxht = getMeasuredHeight() - getExtendedPaddingTop() - |
| getExtendedPaddingBottom(); |
| } |
| int textht = l.getHeight(); |
| |
| if (textht < boxht) { |
| if (gravity == Gravity.BOTTOM) |
| voffset = boxht - textht; |
| else // (gravity == Gravity.CENTER_VERTICAL) |
| voffset = (boxht - textht) >> 1; |
| } |
| } |
| return voffset; |
| } |
| |
| private int getBottomVerticalOffset(boolean forceNormal) { |
| int voffset = 0; |
| final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; |
| |
| Layout l = mLayout; |
| if (!forceNormal && mText.length() == 0 && mHintLayout != null) { |
| l = mHintLayout; |
| } |
| |
| if (gravity != Gravity.BOTTOM) { |
| int boxht; |
| |
| if (l == mHintLayout) { |
| boxht = getMeasuredHeight() - getCompoundPaddingTop() - |
| getCompoundPaddingBottom(); |
| } else { |
| boxht = getMeasuredHeight() - getExtendedPaddingTop() - |
| getExtendedPaddingBottom(); |
| } |
| int textht = l.getHeight(); |
| |
| if (textht < boxht) { |
| if (gravity == Gravity.TOP) |
| voffset = boxht - textht; |
| else // (gravity == Gravity.CENTER_VERTICAL) |
| voffset = (boxht - textht) >> 1; |
| } |
| } |
| return voffset; |
| } |
| |
| private void invalidateCursorPath() { |
| if (mHighlightPathBogus) { |
| invalidateCursor(); |
| } else { |
| synchronized (sTempRect) { |
| /* |
| * The reason for this concern about the thickness of the |
| * cursor and doing the floor/ceil on the coordinates is that |
| * some EditTexts (notably textfields in the Browser) have |
| * anti-aliased text where not all the characters are |
| * necessarily at integer-multiple locations. This should |
| * make sure the entire cursor gets invalidated instead of |
| * sometimes missing half a pixel. |
| */ |
| |
| float thick = FloatMath.ceil(mTextPaint.getStrokeWidth()); |
| if (thick < 1.0f) { |
| thick = 1.0f; |
| } |
| |
| thick /= 2; |
| |
| mHighlightPath.computeBounds(sTempRect, false); |
| |
| int left = getCompoundPaddingLeft(); |
| int top = getExtendedPaddingTop() + getVerticalOffset(true); |
| |
| invalidate((int) FloatMath.floor(left + sTempRect.left - thick), |
| (int) FloatMath.floor(top + sTempRect.top - thick), |
| (int) FloatMath.ceil(left + sTempRect.right + thick), |
| (int) FloatMath.ceil(top + sTempRect.bottom + thick)); |
| } |
| } |
| } |
| |
| private void invalidateCursor() { |
| int where = getSelectionEnd(); |
| |
| invalidateCursor(where, where, where); |
| } |
| |
| private void invalidateCursor(int a, int b, int c) { |
| if (mLayout == null) { |
| invalidate(); |
| } else { |
| if (a >= 0 || b >= 0 || c >= 0) { |
| int first = Math.min(Math.min(a, b), c); |
| int last = Math.max(Math.max(a, b), c); |
| |
| int line = mLayout.getLineForOffset(first); |
| int top = mLayout.getLineTop(line); |
| |
| // This is ridiculous, but the descent from the line above |
| // can hang down into the line we really want to redraw, |
| // so we have to invalidate part of the line above to make |
| // sure everything that needs to be redrawn really is. |
| // (But not the whole line above, because that would cause |
| // the same problem with the descenders on the line above it!) |
| if (line > 0) { |
| top -= mLayout.getLineDescent(line - 1); |
| } |
| |
| int line2; |
| |
| if (first == last) |
| line2 = line; |
| else |
| line2 = mLayout.getLineForOffset(last); |
| |
| int bottom = mLayout.getLineTop(line2 + 1); |
| int voffset = getVerticalOffset(true); |
| |
| int left = getCompoundPaddingLeft() + mScrollX; |
| invalidate(left, top + voffset + getExtendedPaddingTop(), |
| left + getWidth() - getCompoundPaddingLeft() - |
| getCompoundPaddingRight(), |
| bottom + voffset + getExtendedPaddingTop()); |
| } |
| } |
| } |
| |
| private void registerForPreDraw() { |
| final ViewTreeObserver observer = getViewTreeObserver(); |
| if (observer == null) { |
| return; |
| } |
| |
| if (mPreDrawState == PREDRAW_NOT_REGISTERED) { |
| observer.addOnPreDrawListener(this); |
| mPreDrawState = PREDRAW_PENDING; |
| } else if (mPreDrawState == PREDRAW_DONE) { |
| mPreDrawState = PREDRAW_PENDING; |
| } |
| |
| // else state is PREDRAW_PENDING, so keep waiting. |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public boolean onPreDraw() { |
| if (mPreDrawState != PREDRAW_PENDING) { |
| return true; |
| } |
| |
| if (mLayout == null) { |
| assumeLayout(); |
| } |
| |
| boolean changed = false; |
| |
| SelectionModifierCursorController selectionController = null; |
| if (mSelectionModifierCursorController != null) { |
| selectionController = (SelectionModifierCursorController) |
| mSelectionModifierCursorController; |
| } |
| |
| |
| 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(); |
| if (selectionController != null && selectionController.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. |
| // - ExtractEditText does not call onFocus when it is displayed. Fixing this issue would |
| // allow to test for hasSelection in onFocusChanged, which would trigger a |
| // startTextSelectionMode here. TODO |
| if (selectionController != null && hasSelection()) { |
| startTextSelectionMode(); |
| } |
| |
| mPreDrawState = PREDRAW_DONE; |
| return !changed; |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| |
| mTemporaryDetach = false; |
| |
| if (mShowErrorAfterAttach) { |
| showError(); |
| mShowErrorAfterAttach = false; |
| } |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| |
| if (mPreDrawState != PREDRAW_NOT_REGISTERED) { |
| final ViewTreeObserver observer = getViewTreeObserver(); |
| if (observer != null) { |
| observer.removeOnPreDrawListener(this); |
| mPreDrawState = PREDRAW_NOT_REGISTERED; |
| } |
| } |
| |
| if (mError != null) { |
| hideError(); |
| } |
| } |
| |
| @Override |
| protected boolean isPaddingOffsetRequired() { |
| return mShadowRadius != 0 || 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; |
| } |
| return verified; |
| } |
| |
| @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 |
| 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(); |
| } |
| } |
| |
| if (mPreDrawState == PREDRAW_DONE) { |
| final ViewTreeObserver observer = getViewTreeObserver(); |
| if (observer != null) { |
| observer.removeOnPreDrawListener(this); |
| mPreDrawState = PREDRAW_NOT_REGISTERED; |
| } |
| } |
| |
| int color = mCurTextColor; |
| |
| if (mLayout == null) { |
| assumeLayout(); |
| } |
| |
| Layout layout = mLayout; |
| int cursorcolor = color; |
| |
| if (mHint != null && mText.length() == 0) { |
| if (mHintTextColor != null) { |
| color = mCurHintTextColor; |
| } |
| |
| layout = mHintLayout; |
| } |
| |
| mTextPaint.setColor(color); |
| mTextPaint.drawableState = getDrawableState(); |
| |
| canvas.save(); |
| /* Would be faster if we didn't have to do this. Can we chop the |
| (displayable) text so that we don't need to do this ever? |
| */ |
| |
| int extendedPaddingTop = getExtendedPaddingTop(); |
| int extendedPaddingBottom = getExtendedPaddingBottom(); |
| |
| float clipLeft = compoundPaddingLeft + scrollX; |
| float clipTop = extendedPaddingTop + scrollY; |
| float clipRight = right - left - compoundPaddingRight + scrollX; |
| float clipBottom = bottom - top - extendedPaddingBottom + scrollY; |
| |
| if (mShadowRadius != 0) { |
| clipLeft += Math.min(0, mShadowDx - mShadowRadius); |
| clipRight += Math.max(0, mShadowDx + mShadowRadius); |
| |
| clipTop += Math.min(0, mShadowDy - mShadowRadius); |
| clipBottom += Math.max(0, mShadowDy + mShadowRadius); |
| } |
| |
| canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom); |
| |
| int voffsetText = 0; |
| int voffsetCursor = 0; |
| |
| // translate in by our padding |
| { |
| /* shortcircuit calling getVerticaOffset() */ |
| if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { |
| voffsetText = getVerticalOffset(false); |
| voffsetCursor = getVerticalOffset(true); |
| } |
| canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); |
| } |
| |
| if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { |
| if (!mSingleLine && getLineCount() == 1 && canMarquee() && |
| (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { |
| canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft - |
| getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f); |
| } |
| |
| if (mMarquee != null && mMarquee.isRunning()) { |
| canvas.translate(-mMarquee.mScroll, 0.0f); |
| } |
| } |
| |
| Path highlight = null; |
| int selStart = -1, selEnd = -1; |
| |
| // If there is no movement method, then there can be no selection. |
| // Check that first and attempt to skip everything having to do with |
| // the cursor. |
| // XXX This is not strictly true -- a program could set the |
| // selection manually if it really wanted to. |
| if (mMovement != null && (isFocused() || isPressed())) { |
| selStart = getSelectionStart(); |
| selEnd = getSelectionEnd(); |
| |
| if (mCursorVisible && selStart >= 0 && isEnabled()) { |
| if (mHighlightPath == null) |
| mHighlightPath = new Path(); |
| |
| if (selStart == selEnd) { |
| if ((SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) { |
| if (mHighlightPathBogus) { |
| mHighlightPath.reset(); |
| mLayout.getCursorPath(selStart, mHighlightPath, mText); |
| mHighlightPathBogus = false; |
| } |
| |
| // XXX should pass to skin instead of drawing directly |
| mHighlightPaint.setColor(cursorcolor); |
| mHighlightPaint.setStyle(Paint.Style.STROKE); |
| |
| highlight = mHighlightPath; |
| } |
| } else { |
| if (mHighlightPathBogus) { |
| mHighlightPath.reset(); |
| mLayout.getSelectionPath(selStart, selEnd, mHighlightPath); |
| mHighlightPathBogus = false; |
| } |
| |
| // XXX should pass to skin instead of drawing directly |
| mHighlightPaint.setColor(mHighlightColor); |
| mHighlightPaint.setStyle(Paint.Style.FILL); |
| |
| highlight = mHighlightPath; |
| } |
| } |
| } |
| |
| /* Comment out until we decide what to do about animations |
| boolean isLinearTextOn = false; |
| if (currentTransformation != null) { |
| isLinearTextOn = mTextPaint.isLinearTextOn(); |
| Matrix m = currentTransformation.getMatrix(); |
| if (!m.isIdentity()) { |
| // mTextPaint.setLinearTextOn(true); |
| } |
| } |
| */ |
| |
| final InputMethodState ims = mInputMethodState; |
| if (ims != null && ims.mBatchEditNesting == 0) { |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| if (imm != null) { |
| if (imm.isActive(this)) { |
| boolean reported = false; |
| if (ims.mContentChanged || ims.mSelectionModeChanged) { |
| // We are in extract mode and the content has changed |
| // in some way... just report complete new text to the |
| // input method. |
| reported = reportExtractedText(); |
| } |
| if (!reported && highlight != null) { |
| int candStart = -1; |
| int candEnd = -1; |
| if (mText instanceof Spannable) { |
| Spannable sp = (Spannable)mText; |
| candStart = EditableInputConnection.getComposingSpanStart(sp); |
| candEnd = EditableInputConnection.getComposingSpanEnd(sp); |
| } |
| imm.updateSelection(this, selStart, selEnd, candStart, candEnd); |
| } |
| } |
| |
| if (imm.isWatchingCursor(this) && highlight != null) { |
| highlight.computeBounds(ims.mTmpRectF, true); |
| ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0; |
| |
| canvas.getMatrix().mapPoints(ims.mTmpOffset); |
| ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]); |
| |
| ims.mTmpRectF.offset(0, voffsetCursor - voffsetText); |
| |
| ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5), |
| (int)(ims.mTmpRectF.top + 0.5), |
| (int)(ims.mTmpRectF.right + 0.5), |
| (int)(ims.mTmpRectF.bottom + 0.5)); |
| |
| imm.updateCursor(this, |
| ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top, |
| ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom); |
| } |
| } |
| } |
| |
| layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText); |
| |
| if (mMarquee != null && mMarquee.shouldDrawGhost()) { |
| canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); |
| layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText); |
| } |
| |
| /* Comment out until we decide what to do about animations |
| if (currentTransformation != null) { |
| mTextPaint.setLinearTextOn(isLinearTextOn); |
| } |
| */ |
| |
| canvas.restore(); |
| |
| if (mInsertionPointCursorController != null) { |
| mInsertionPointCursorController.draw(canvas); |
| } |
| if (mSelectionModifierCursorController != null) { |
| mSelectionModifierCursorController.draw(canvas); |
| } |
| } |
| |
| @Override |
| public void getFocusedRect(Rect r) { |
| if (mLayout == null) { |
| super.getFocusedRect(r); |
| return; |
| } |
| |
| int sel = getSelectionEnd(); |
| if (sel < 0) { |
| super.getFocusedRect(r); |
| return; |
| } |
| |
| int line = mLayout.getLineForOffset(sel); |
| r.top = mLayout.getLineTop(line); |
| r.bottom = mLayout.getLineBottom(line); |
| |
| r.left = (int) mLayout.getPrimaryHorizontal(sel); |
| r.right = r.left + 1; |
| |
| // Adjust for padding and gravity. |
| int paddingLeft = getCompoundPaddingLeft(); |
| int paddingTop = getExtendedPaddingTop(); |
| if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { |
| paddingTop += getVerticalOffset(false); |
| } |
| r.offset(paddingLeft, paddingTop); |
| } |
| |
| /** |
| * Return the number of lines of text, or 0 if the internal Layout has not |
| * been built. |
| */ |
| public int getLineCount() { |
| return mLayout != null ? mLayout.getLineCount() : 0; |
| } |
| |
| /** |
| * Return the baseline for the specified line (0...getLineCount() - 1) |
| * If bounds is not null, return the top, left, right, bottom extents |
| * of the specified line in it. If the internal Layout has not been built, |
| * return 0 and set bounds to (0, 0, 0, 0) |
| * @param line which line to examine (0..getLineCount() - 1) |
| * @param bounds Optional. If not null, it returns the extent of the line |
| * @return the Y-coordinate of the baseline |
| */ |
| public int getLineBounds(int line, Rect bounds) { |
| if (mLayout == null) { |
| if (bounds != null) { |
| bounds.set(0, 0, 0, 0); |
| } |
| return 0; |
| } |
| else { |
| int baseline = mLayout.getLineBounds(line, bounds); |
| |
| int voffset = getExtendedPaddingTop(); |
| if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { |
| voffset += getVerticalOffset(true); |
| } |
| if (bounds != null) { |
| bounds.offset(getCompoundPaddingLeft(), voffset); |
| } |
| return baseline + voffset; |
| } |
| } |
| |
| @Override |
| public int getBaseline() { |
| if (mLayout == null) { |
| return super.getBaseline(); |
| } |
| |
| int voffset = 0; |
| if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { |
| voffset = getVerticalOffset(true); |
| } |
| |
| return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0); |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| int which = doKeyDown(keyCode, event, null); |
| if (which == 0) { |
| // Go through default dispatching. |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { |
| KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN); |
| |
| int which = doKeyDown(keyCode, down, event); |
| if (which == 0) { |
| // Go through default dispatching. |
| return super.onKeyMultiple(keyCode, repeatCount, event); |
| } |
| if (which == -1) { |
| // Consumed the whole thing. |
| return true; |
| } |
| |
| repeatCount--; |
| |
| // We are going to dispatch the remaining events to either the input |
| // or movement method. To do this, we will just send a repeated stream |
| // of down and up events until we have done the complete repeatCount. |
| // It would be nice if those interfaces had an onKeyMultiple() method, |
| // but adding that is a more complicated change. |
| KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP); |
| if (which == 1) { |
| mInput.onKeyUp(this, (Editable)mText, keyCode, up); |
| while (--repeatCount > 0) { |
| mInput.onKeyDown(this, (Editable)mText, keyCode, down); |
| mInput.onKeyUp(this, (Editable)mText, keyCode, up); |
| } |
| if (mError != null && !mErrorWasChanged) { |
| setError(null, null); |
| } |
| |
| } else if (which == 2) { |
| mMovement.onKeyUp(this, (Spannable)mText, keyCode, up); |
| while (--repeatCount > 0) { |
| mMovement.onKeyDown(this, (Spannable)mText, keyCode, down); |
| mMovement.onKeyUp(this, (Spannable)mText, keyCode, up); |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Returns true if pressing ENTER in this field advances focus instead |
| * of inserting the character. This is true mostly in single-line fields, |
| * but also in mail addresses and subjects which will display on multiple |
| * lines but where it doesn't make sense to insert newlines. |
| */ |
| private boolean shouldAdvanceFocusOnEnter() { |
| if (mInput == null) { |
| return false; |
| } |
| |
| if (mSingleLine) { |
| return true; |
| } |
| |
| if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { |
| int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION; |
| |
| if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || |
| variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) { |
| if (!isEnabled()) { |
| return 0; |
| } |
| |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_ENTER: |
| // If ALT modifier is held, then we always insert a |
| // newline character. |
| if ((event.getMetaState()&KeyEvent.META_ALT_ON) == 0) { |
| |
| // When mInputContentType is set, we know that we are |
| // running in a "modern" cupcake environment, so don't need |
| // to worry about the application trying to capture |
| // enter key events. |
| if (mInputContentType != null) { |
| |
| // If there is an action listener, given them a |
| // chance to consume the event. |
| if (mInputContentType.onEditorActionListener != null && |
| mInputContentType.onEditorActionListener.onEditorAction( |
| this, EditorInfo.IME_NULL, event)) { |
| mInputContentType.enterDown = true; |
| // We are consuming the enter key for them. |
| return -1; |
| } |
| } |
| |
| // If our editor should move focus when enter is pressed, or |
| // this is a generated event from an IME action button, then |
| // don't let it be inserted into the text. |
| if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0 |
| || shouldAdvanceFocusOnEnter()) { |
| return -1; |
| } |
| } |
| break; |
| |
| case KeyEvent.KEYCODE_DPAD_CENTER: |
| if (shouldAdvanceFocusOnEnter()) { |
| return 0; |
| } |
| break; |
| |
| // Has to be done on key down (and not on key up) to correctly be intercepted. |
| case KeyEvent.KEYCODE_BACK: |
| if (mIsInTextSelectionMode) { |
| stopTextSelectionMode(); |
| return -1; |
| } |
| break; |
| } |
| |
| if (mInput != null) { |
| /* |
| * Keep track of what the error was before doing the input |
| * so that if an input filter changed the error, we leave |
| * that error showing. Otherwise, we take down whatever |
| * error was showing when the user types something. |
| */ |
| mErrorWasChanged = false; |
| |
| boolean doDown = true; |
| if (otherEvent != null) { |
| try { |
| beginBatchEdit(); |
| boolean handled = mInput.onKeyOther(this, (Editable) mText, |
| otherEvent); |
| if (mError != null && !mErrorWasChanged) { |
| setError(null, null); |
| } |
| doDown = false; |
| if (handled) { |
| return -1; |
| } |
| } catch (AbstractMethodError e) { |
| // onKeyOther was added after 1.0, so if it isn't |
| // implemented we need to try to dispatch as a regular down. |
| } finally { |
| endBatchEdit(); |
| } |
| } |
| |
| if (doDown) { |
| beginBatchEdit(); |
| if (mInput.onKeyDown(this, (Editable) mText, keyCode, event)) { |
| endBatchEdit(); |
| if (mError != null && !mErrorWasChanged) { |
| setError(null, null); |
| } |
| return 1; |
| } |
| endBatchEdit(); |
| } |
| } |
| |
| // bug 650865: sometimes we get a key event before a layout. |
| // don't try to move around if we don't know the layout. |
| |
| if (mMovement != null && mLayout != null) { |
| boolean doDown = true; |
| if (otherEvent != null) { |
| try { |
| boolean handled = mMovement.onKeyOther(this, (Spannable) mText, |
| otherEvent); |
| doDown = false; |
| if (handled) { |
| return -1; |
| } |
| } catch (AbstractMethodError e) { |
| // onKeyOther was added after 1.0, so if it isn't |
| // implemented we need to try to dispatch as a regular down. |
| } |
| } |
| if (doDown) { |
| if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event)) |
| return 2; |
| } |
| } |
| |
| return 0; |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (!isEnabled()) { |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| hideControllers(); |
| |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_DPAD_CENTER: |
| /* |
| * If there is a click listener, just call through to |
| * super, which will invoke it. |
| * |
| * If there isn't a click listener, try to show the soft |
| * input method. (It will also |
| * call performClick(), but that won't do anything in |
| * this case.) |
| */ |
| if (mOnClickListener == null) { |
| if (mMovement != null && mText instanceof Editable |
| && mLayout != null && onCheckIsTextEditor()) { |
| InputMethodManager imm = (InputMethodManager) |
| getContext().getSystemService(Context.INPUT_METHOD_SERVICE); |
| imm.showSoftInput(this, 0); |
| } |
| } |
| return super.onKeyUp(keyCode, event); |
| |
| case KeyEvent.KEYCODE_ENTER: |
| if (mInputContentType != null |
| && mInputContentType.onEditorActionListener != null |
| && mInputContentType.enterDown) { |
| mInputContentType.enterDown = false; |
| if (mInputContentType.onEditorActionListener.onEditorAction( |
| this, EditorInfo.IME_NULL, event)) { |
| return true; |
| } |
| } |
| |
| if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0 |
| || shouldAdvanceFocusOnEnter()) { |
| /* |
| * If there is a click listener, just call through to |
| * super, which will invoke it. |
| * |
| * If there isn't a click listener, try to advance focus, |
| * but still call through to super, which will reset the |
| * pressed state and longpress state. (It will also |
| * call performClick(), but that won't do anything in |
| * this case.) |
| */ |
| if (mOnClickListener == null) { |
| View v = focusSearch(FOCUS_DOWN); |
| |
| if (v != null) { |
| if (!v.requestFocus(FOCUS_DOWN)) { |
| throw new IllegalStateException("focus search returned a view " + |
| "that wasn't able to take focus!"); |
| } |
| |
| /* |
| * Return true because we handled the key; super |
| * will return false because there was no click |
| * listener. |
| */ |
| super.onKeyUp(keyCode, event); |
| return true; |
| } else if ((event.getFlags() |
| & KeyEvent.FLAG_EDITOR_ACTION) != 0) { |
| // No target for next focus, but make sure the IME |
| // if this came from it. |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| if (imm != null) { |
| imm.hideSoftInputFromWindow(getWindowToken(), 0); |
| } |
| } |
| } |
| |
| return super.onKeyUp(keyCode, event); |
| } |
| break; |
| } |
| |
| if (mInput != null) |
| if (mInput.onKeyUp(this, (Editable) mText, keyCode, event)) |
| return true; |
| |
| if (mMovement != null && mLayout != null) |
| if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event)) |
| return true; |
| |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| @Override public boolean onCheckIsTextEditor() { |
| return mInputType != EditorInfo.TYPE_NULL; |
| } |
| |
| @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { |
| if (onCheckIsTextEditor()) { |
| if (mInputMethodState == null) { |
| mInputMethodState = new InputMethodState(); |
| } |
| outAttrs.inputType = mInputType; |
| if (mInputContentType != null) { |
| outAttrs.imeOptions = mInputContentType.imeOptions; |
| outAttrs.privateImeOptions = mInputContentType.privateImeOptions; |
| outAttrs.actionLabel = mInputContentType.imeActionLabel; |
| outAttrs.actionId = mInputContentType.imeActionId; |
| outAttrs.extras = mInputContentType.extras; |
| } else { |
| outAttrs.imeOptions = EditorInfo.IME_NULL; |
| } |
| if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION) |
| == EditorInfo.IME_ACTION_UNSPECIFIED) { |
| if (focusSearch(FOCUS_DOWN) != null) { |
| // An action has not been set, but the enter key will move to |
| // the next focus, so set the action to that. |
| outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; |
| } 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 ((outAttrs.inputType & (InputType.TYPE_MASK_CLASS |
| | InputType.TYPE_TEXT_FLAG_MULTI_LINE)) |
| == (InputType.TYPE_CLASS_TEXT |
| | InputType.TYPE_TEXT_FLAG_MULTI_LINE)) { |
| // Multi-line text editors should always show an enter key. |
| outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; |
| } |
| outAttrs.hintText = mHint; |
| if (mText instanceof Editable) { |
| InputConnection ic = new EditableInputConnection(this); |
| outAttrs.initialSelStart = getSelectionStart(); |
| outAttrs.initialSelEnd = getSelectionEnd(); |
| outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType); |
| return ic; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * If this TextView contains editable content, extract a portion of it |
| * based on the information in <var>request</var> in to <var>outText</var>. |
| * @return Returns true if the text was successfully extracted, else false. |
| */ |
| public boolean extractText(ExtractedTextRequest request, |
| ExtractedText outText) { |
| return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, |
| EXTRACT_UNKNOWN, outText); |
| } |
| |
| static final int EXTRACT_NOTHING = -2; |
| static final int EXTRACT_UNKNOWN = -1; |
| |
| boolean extractTextInternal(ExtractedTextRequest request, |
| int partialStartOffset, int partialEndOffset, int delta, |
| ExtractedText outText) { |
| final CharSequence content = mText; |
| if (content != null) { |
| if (partialStartOffset != EXTRACT_NOTHING) { |
| final int N = content.length(); |
| if (partialStartOffset < 0) { |
| outText.partialStartOffset = outText.partialEndOffset = -1; |
| partialStartOffset = 0; |
| partialEndOffset = N; |
| } else { |
| // Adjust offsets to ensure we contain full spans. |
| if (content instanceof Spanned) { |
| Spanned spanned = (Spanned)content; |
| Object[] spans = spanned.getSpans(partialStartOffset, |
| partialEndOffset, ParcelableSpan.class); |
| int i = spans.length; |
| while (i > 0) { |
| i--; |
| int j = spanned.getSpanStart(spans[i]); |
| if (j < partialStartOffset) partialStartOffset = j; |
| j = spanned.getSpanEnd(spans[i]); |
| if (j > partialEndOffset) partialEndOffset = j; |
| } |
| } |
| outText.partialStartOffset = partialStartOffset; |
| outText.partialEndOffset = partialEndOffset; |
| // Now use the delta to determine the actual amount of text |
| // we need. |
| partialEndOffset += delta; |
| if (partialStartOffset > N) { |
| partialStartOffset = N; |
| } else if (partialStartOffset < 0) { |
| partialStartOffset = 0; |
| } |
| if (partialEndOffset > N) { |
| partialEndOffset = N; |
| } else if (partialEndOffset < 0) { |
| partialEndOffset = 0; |
| } |
| } |
| if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { |
| outText.text = content.subSequence(partialStartOffset, |
| partialEndOffset); |
| } else { |
| outText.text = TextUtils.substring(content, partialStartOffset, |
| partialEndOffset); |
| } |
| } else { |
| outText.partialStartOffset = 0; |
| outText.partialEndOffset = 0; |
| outText.text = ""; |
| } |
| outText.flags = 0; |
| if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) { |
| outText.flags |= ExtractedText.FLAG_SELECTING; |
| } |
| if (mSingleLine) { |
| outText.flags |= ExtractedText.FLAG_SINGLE_LINE; |
| } |
| outText.startOffset = 0; |
| outText.selectionStart = getSelectionStart(); |
| outText.selectionEnd = getSelectionEnd(); |
| return true; |
| } |
| return false; |
| } |
| |
| boolean reportExtractedText() { |
| final InputMethodState ims = mInputMethodState; |
| if (ims != null) { |
| final boolean contentChanged = ims.mContentChanged; |
| if (contentChanged || ims.mSelectionModeChanged) { |
| ims.mContentChanged = false; |
| ims.mSelectionModeChanged = false; |
| final ExtractedTextRequest req = mInputMethodState.mExtracting; |
| if (req != null) { |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| if (imm != null) { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start=" |
| + ims.mChangedStart + " end=" + ims.mChangedEnd |
| + " delta=" + ims.mChangedDelta); |
| if (ims.mChangedStart < 0 && !contentChanged) { |
| ims.mChangedStart = EXTRACT_NOTHING; |
| } |
| if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, |
| ims.mChangedDelta, ims.mTmpExtracted)) { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start=" |
| + ims.mTmpExtracted.partialStartOffset |
| + " end=" + ims.mTmpExtracted.partialEndOffset |
| + ": " + ims.mTmpExtracted.text); |
| imm.updateExtractedText(this, req.token, |
| mInputMethodState.mTmpExtracted); |
| return true; |
| } |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * This is used to remove all style-impacting spans from text before new |
| * extracted text is being replaced into it, so that we don't have any |
| * lingering spans applied during the replace. |
| */ |
| static void removeParcelableSpans(Spannable spannable, int start, int end) { |
| Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class); |
| int i = spans.length; |
| while (i > 0) { |
| i--; |
| spannable.removeSpan(spans[i]); |
| } |
| } |
| |
| /** |
| * Apply to this text view the given extracted text, as previously |
| * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}. |
| */ |
| public void setExtractedText(ExtractedText text) { |
| Editable content = getEditableText(); |
| if (text.text != null) { |
| if (content == null) { |
| setText(text.text, TextView.BufferType.EDITABLE); |
| } else if (text.partialStartOffset < 0) { |
| removeParcelableSpans(content, 0, content.length()); |
| content.replace(0, content.length(), text.text); |
| } else { |
| final int N = content.length(); |
| int start = text.partialStartOffset; |
| if (start > N) start = N; |
| int end = text.partialEndOffset; |
| if (end > N) end = N; |
| removeParcelableSpans(content, start, end); |
| content.replace(start, end, text.text); |
| } |
| } |
| |
| // Now set the selection position... make sure it is in range, to |
| // avoid crashes. If this is a partial update, it is possible that |
| // the underlying text may have changed, causing us problems here. |
| // Also we just don't want to trust clients to do the right thing. |
| Spannable sp = (Spannable)getText(); |
| final int N = sp.length(); |
| int start = text.selectionStart; |
| if (start < 0) start = 0; |
| else if (start > N) start = N; |
| int end = text.selectionEnd; |
| if (end < 0) end = 0; |
| else if (end > N) end = N; |
| Selection.setSelection(sp, start, end); |
| |
| // Finally, update the selection mode. |
| if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) { |
| MetaKeyKeyListener.startSelecting(this, sp); |
| } else { |
| MetaKeyKeyListener.stopSelecting(this, sp); |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| public void setExtracting(ExtractedTextRequest req) { |
| if (mInputMethodState != null) { |
| mInputMethodState.mExtracting = req; |
| } |
| } |
| |
| /** |
| * Called by the framework in response to a text completion from |
| * the current input method, provided by it calling |
| * {@link InputConnection#commitCompletion |
| * InputConnection.commitCompletion()}. The default implementation does |
| * nothing; text views that are supporting auto-completion should override |
| * this to do their desired behavior. |
| * |
| * @param text The auto complete text the user has selected. |
| */ |
| public void onCommitCompletion(CompletionInfo text) { |
| } |
| |
| public void beginBatchEdit() { |
| final InputMethodState ims = mInputMethodState; |
| if (ims != null) { |
| int nesting = ++ims.mBatchEditNesting; |
| if (nesting == 1) { |
| ims.mCursorChanged = false; |
| ims.mChangedDelta = 0; |
| if (ims.mContentChanged) { |
| // We already have a pending change from somewhere else, |
| // so turn this into a full update. |
| ims.mChangedStart = 0; |
| ims.mChangedEnd = mText.length(); |
| } else { |
| ims.mChangedStart = EXTRACT_UNKNOWN; |
| ims.mChangedEnd = EXTRACT_UNKNOWN; |
| ims.mContentChanged = false; |
| } |
| onBeginBatchEdit(); |
| } |
| } |
| } |
| |
| public void endBatchEdit() { |
| final InputMethodState ims = mInputMethodState; |
| if (ims != null) { |
| int nesting = --ims.mBatchEditNesting; |
| if (nesting == 0) { |
| finishBatchEdit(ims); |
| } |
| } |
| } |
| |
| void ensureEndedBatchEdit() { |
| final InputMethodState ims = mInputMethodState; |
| if (ims != null && ims.mBatchEditNesting != 0) { |
| ims.mBatchEditNesting = 0; |
| finishBatchEdit(ims); |
| } |
| } |
| |
| void finishBatchEdit(final InputMethodState ims) { |
| onEndBatchEdit(); |
| |
| if (ims.mContentChanged || ims.mSelectionModeChanged) { |
| updateAfterEdit(); |
| reportExtractedText(); |
| } else if (ims.mCursorChanged) { |
| // Cheezy way to get us to report the current cursor location. |
| invalidateCursor(); |
| } |
| } |
| |
| void updateAfterEdit() { |
| invalidate(); |
| int curs = getSelectionStart(); |
| |
| if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == |
| Gravity.BOTTOM) { |
| registerForPreDraw(); |
| } |
| |
| if (curs >= 0) { |
| mHighlightPathBogus = true; |
| |
| if (isFocused()) { |
| mShowCursor = SystemClock.uptimeMillis(); |
| makeBlink(); |
| } |
| } |
| |
| checkForResize(); |
| } |
| |
| /** |
| * Called by the framework in response to a request to begin a batch |
| * of edit operations through a call to link {@link #beginBatchEdit()}. |
| */ |
| public void onBeginBatchEdit() { |
| } |
| |
| /** |
| * Called by the framework in response to a request to end a batch |
| * of edit operations through a call to link {@link #endBatchEdit}. |
| */ |
| public void onEndBatchEdit() { |
| } |
| |
| /** |
| * Called by the framework in response to a private command from the |
| * current method, provided by it calling |
| * {@link InputConnection#performPrivateCommand |
| * InputConnection.performPrivateCommand()}. |
| * |
| * @param action The action name of the command. |
| * @param data Any additional data for the command. This may be null. |
| * @return Return true if you handled the command, else false. |
| */ |
| public boolean onPrivateIMECommand(String action, Bundle data) { |
| return false; |
| } |
| |
| private void nullLayouts() { |
| if (mLayout instanceof BoringLayout && mSavedLayout == null) { |
| mSavedLayout = (BoringLayout) mLayout; |
| } |
| if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) { |
| mSavedHintLayout = (BoringLayout) mHintLayout; |
| } |
| |
| mLayout = mHintLayout = null; |
| } |
| |
| /** |
| * Make a new Layout based on the already-measured size of the view, |
| * on the assumption that it was measured correctly at some point. |
| */ |
| private void assumeLayout() { |
| int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); |
| |
| if (width < 1) { |
| width = 0; |
| } |
| |
| int physicalWidth = width; |
| |
| if (mHorizontallyScrolling) { |
| width = VERY_WIDE; |
| } |
| |
| makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING, |
| physicalWidth, false); |
| } |
| |
| /** |
| * The width passed in is now the desired layout width, |
| * not the full view width with padding. |
| * {@hide} |
| */ |
| protected void makeNewLayout(int w, int hintWidth, |
| BoringLayout.Metrics boring, |
| BoringLayout.Metrics hintBoring, |
| int ellipsisWidth, boolean bringIntoView) { |
| stopMarquee(); |
| |
| mHighlightPathBogus = true; |
| |
| if (w < 0) { |
| w = 0; |
| } |
| if (hintWidth < 0) { |
| hintWidth = 0; |
| } |
| |
| Layout.Alignment alignment; |
| switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { |
| case Gravity.CENTER_HORIZONTAL: |
| alignment = Layout.Alignment.ALIGN_CENTER; |
| break; |
| |
| case Gravity.RIGHT: |
| alignment = Layout.Alignment.ALIGN_OPPOSITE; |
| break; |
| |
| default: |
| alignment = Layout.Alignment.ALIGN_NORMAL; |
| } |
| |
| boolean shouldEllipsize = mEllipsize != null && mInput == null; |
| |
| if (mText instanceof Spannable) { |
| mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w, |
| alignment, mSpacingMult, |
| mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null, |
| ellipsisWidth); |
| } else { |
| if (boring == UNKNOWN_BORING) { |
| boring = BoringLayout.isBoring(mTransformed, mTextPaint, |
| mBoring); |
| if (boring != null) { |
| mBoring = boring; |
| } |
| } |
| |
| if (boring != null) { |
| if (boring.width <= w && |
| (mEllipsize == null || boring.width <= ellipsisWidth)) { |
| if (mSavedLayout != null) { |
| mLayout = mSavedLayout. |
| replaceOrMake(mTransformed, mTextPaint, |
| w, alignment, mSpacingMult, mSpacingAdd, |
| boring, mIncludePad); |
| } else { |
| mLayout = BoringLayout.make(mTransformed, mTextPaint, |
| w, alignment, mSpacingMult, mSpacingAdd, |
| boring, mIncludePad); |
| } |
| |
| mSavedLayout = (BoringLayout) mLayout; |
| } else if (shouldEllipsize && boring.width <= w) { |
| if (mSavedLayout != null) { |
| mLayout = mSavedLayout. |
| replaceOrMake(mTransformed, mTextPaint, |
| w, alignment, mSpacingMult, mSpacingAdd, |
| boring, mIncludePad, mEllipsize, |
| ellipsisWidth); |
| } else { |
| mLayout = BoringLayout.make(mTransformed, mTextPaint, |
| w, alignment, mSpacingMult, mSpacingAdd, |
| boring, mIncludePad, mEllipsize, |
| ellipsisWidth); |
| } |
| } else if (shouldEllipsize) { |
| mLayout = new StaticLayout(mTransformed, |
| 0, mTransformed.length(), |
| mTextPaint, w, alignment, mSpacingMult, |
| mSpacingAdd, mIncludePad, mEllipsize, |
| ellipsisWidth); |
| } else { |
| mLayout = new StaticLayout(mTransformed, mTextPaint, |
| w, alignment, mSpacingMult, mSpacingAdd, |
| mIncludePad); |
| } |
| } else if (shouldEllipsize) { |
| mLayout = new StaticLayout(mTransformed, |
| 0, mTransformed.length(), |
| mTextPaint, w, alignment, mSpacingMult, |
| mSpacingAdd, mIncludePad, mEllipsize, |
| ellipsisWidth); |
| } else { |
| mLayout = new StaticLayout(mTransformed, mTextPaint, |
| w, alignment, mSpacingMult, mSpacingAdd, |
| mIncludePad); |
| } |
| } |
| |
| shouldEllipsize = mEllipsize != null; |
| mHintLayout = null; |
| |
| if (mHint != null) { |
| if (shouldEllipsize) hintWidth = w; |
| |
| if (hintBoring == UNKNOWN_BORING) { |
| hintBoring = BoringLayout.isBoring(mHint, mTextPaint, |
| 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, mSpacingMult, |
| mSpacingAdd, mIncludePad, mEllipsize, |
| ellipsisWidth); |
| } else { |
| mHintLayout = new StaticLayout(mHint, mTextPaint, |
| hintWidth, alignment, mSpacingMult, mSpacingAdd, |
| mIncludePad); |
| } |
| } else if (shouldEllipsize) { |
| mHintLayout = new StaticLayout(mHint, |
| 0, mHint.length(), |
| mTextPaint, hintWidth, alignment, mSpacingMult, |
| mSpacingAdd, mIncludePad, mEllipsize, |
| ellipsisWidth); |
| } else { |
| mHintLayout = new StaticLayout(mHint, mTextPaint, |
| hintWidth, alignment, mSpacingMult, mSpacingAdd, |
| mIncludePad); |
| } |
| } |
| |
| if (bringIntoView) { |
| registerForPreDraw(); |
| } |
| |
| if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { |
| if (!compressText(ellipsisWidth)) { |
| final int height = mLayoutParams.height; |
| // If the size of the view does not depend on the size of the text, try to |
| // start the marquee immediately |
| if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) { |
| startMarquee(); |
| } else { |
| // Defer the start of the marquee until we know our width (see setFrame()) |
| mRestartMarquee = true; |
| } |
| } |
| } |
| |
| // CursorControllers need a non-null mLayout |
| prepareCursorControllers(); |
| } |
| |
| private boolean compressText(float width) { |
| // Only compress the text if it hasn't been compressed by the previous pass |
| if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX && |
| mTextPaint.getTextScaleX() == 1.0f) { |
| final float textWidth = mLayout.getLineWidth(0); |
| final float overflow = (textWidth + 1.0f - width) / width; |
| if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) { |
| mTextPaint.setTextScaleX(1.0f - overflow - 0.005f); |
| post(new Runnable() { |
| public void run() { |
| requestLayout(); |
| } |
| }); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private static int desired(Layout layout) { |
| int n = layout.getLineCount(); |
| CharSequence text = layout.getText(); |
| float max = 0; |
| |
| // if any line was wrapped, we can't use it. |
| // but it's ok for the last line not to have a newline |
| |
| for (int i = 0; i < n - 1; i++) { |
| if (text.charAt(layout.getLineEnd(i) - 1) != '\n') |
| return -1; |
| } |
| |
| for (int i = 0; i < n; i++) { |
| max = Math.max(max, layout.getLineWidth(i)); |
| } |
| |
| return (int) FloatMath.ceil(max); |
| } |
| |
| /** |
| * Set whether the TextView includes extra top and bottom padding to make |
| * room for accents that go above the normal ascent and descent. |
| * The default is true. |
| * |
| * @attr ref android.R.styleable#TextView_includeFontPadding |
| */ |
| public void setIncludeFontPadding(boolean includepad) { |
| mIncludePad = includepad; |
| |
| if (mLayout != null) { |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| |
| private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics(); |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
| int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| int widthSize = MeasureSpec.getSize(widthMeasureSpec); |
| int heightSize = MeasureSpec.getSize(heightMeasureSpec); |
| |
| int width; |
| int height; |
| |
| BoringLayout.Metrics boring = UNKNOWN_BORING; |
| BoringLayout.Metrics hintBoring = UNKNOWN_BORING; |
| |
| int des = -1; |
| boolean fromexisting = false; |
| |
| if (widthMode == MeasureSpec.EXACTLY) { |
| // Parent has told us how big to be. So be it. |
| width = widthSize; |
| } else { |
| if (mLayout != null && mEllipsize == null) { |
| des = desired(mLayout); |
| } |
| |
| if (des < 0) { |
| boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring); |
| if (boring != null) { |
| mBoring = boring; |
| } |
| } else { |
| fromexisting = true; |
| } |
| |
| if (boring == null || boring == UNKNOWN_BORING) { |
| if (des < 0) { |
| des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint)); |
| } |
| |
| width = des; |
| } else { |
| width = boring.width; |
| } |
| |
| final Drawables dr = mDrawables; |
| if (dr != null) { |
| width = Math.max(width, dr.mDrawableWidthTop); |
| width = Math.max(width, dr.mDrawableWidthBottom); |
| } |
| |
| if (mHint != null) { |
| int hintDes = -1; |
| int hintWidth; |
| |
| if (mHintLayout != null && mEllipsize == null) { |
| hintDes = desired(mHintLayout); |
| } |
| |
| if (hintDes < 0) { |
| hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring); |
| if (hintBoring != null) { |
| mHintBoring = hintBoring; |
| } |
| } |
| |
| if (hintBoring == null || hintBoring == UNKNOWN_BORING) { |
| if (hintDes < 0) { |
| hintDes = (int) FloatMath.ceil( |
| Layout.getDesiredWidth(mHint, mTextPaint)); |
| } |
| |
| hintWidth = hintDes; |
| } else { |
| hintWidth = hintBoring.width; |
| } |
| |
| if (hintWidth > width) { |
| width = hintWidth; |
| } |
| } |
| |
| width += getCompoundPaddingLeft() + getCompoundPaddingRight(); |
| |
| if (mMaxWidthMode == EMS) { |
| width = Math.min(width, mMaxWidth * getLineHeight()); |
| } else { |
| width = Math.min(width, mMaxWidth); |
| } |
| |
| if (mMinWidthMode == EMS) { |
| width = Math.max(width, mMinWidth * getLineHeight()); |
| } else { |
| width = Math.max(width, mMinWidth); |
| } |
| |
| // Check against our minimum width |
| width = Math.max(width, getSuggestedMinimumWidth()); |
| |
| if (widthMode == MeasureSpec.AT_MOST) { |
| width = Math.min(widthSize, width); |
| } |
| } |
| |
| int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight(); |
| int unpaddedWidth = want; |
| int hintWant = want; |
| |
| if (mHorizontallyScrolling) |
| want = VERY_WIDE; |
| |
| int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth(); |
| |
| if (mLayout == null) { |
| makeNewLayout(want, hintWant, boring, hintBoring, |
| width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); |
| } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) || |
| (mLayout.getEllipsizedWidth() != |
| width - getCompoundPaddingLeft() - getCompoundPaddingRight())) { |
| if (mHint == null && mEllipsize == null && |
| want > mLayout.getWidth() && |
| (mLayout instanceof BoringLayout || |
| (fromexisting && des >= 0 && des <= want))) { |
| mLayout.increaseWidthTo(want); |
| } else { |
| makeNewLayout(want, hintWant, boring, hintBoring, |
| width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); |
| } |
| } else { |
| // Width has not changed. |
| } |
| |
| if (heightMode == MeasureSpec.EXACTLY) { |
| // Parent has told us how big to be. So be it. |
| height = heightSize; |
| mDesiredHeightAtMeasure = -1; |
| } else { |
| int desired = getDesiredHeight(); |
| |
| height = desired; |
| mDesiredHeightAtMeasure = desired; |
| |
| if (heightMode == MeasureSpec.AT_MOST) { |
| height = Math.min(desired, 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) + |
| layout.getBottomPadding(); |
| |
| if (dr != null) { |
| desired = Math.max(desired, dr.mDrawableHeightLeft); |
| desired = Math.max(desired, dr.mDrawableHeightRight); |
| } |
| |
| desired += pad; |
| linecount = mMaximum; |
| } |
| } |
| } else { |
| desired = Math.min(desired, mMaximum); |
| } |
| |
| if (mMinMode == LINES) { |
| if (linecount < mMinimum) { |
| desired += getLineHeight() * (mMinimum - linecount); |
| } |
| } else { |
| desired = Math.max(desired, mMinimum); |
| } |
| |
| // Check against our minimum height |
| desired = Math.max(desired, getSuggestedMinimumHeight()); |
| |
| return desired; |
| } |
| |
| /** |
| * Check whether a change to the existing text layout requires a |
| * new view layout. |
| */ |
| private void checkForResize() { |
| boolean sizeChanged = false; |
| |
| if (mLayout != null) { |
| // Check if our width changed |
| if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) { |
| sizeChanged = true; |
| invalidate(); |
| } |
| |
| // Check if our height changed |
| if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) { |
| int desiredHeight = getDesiredHeight(); |
| |
| if (desiredHeight != this.getHeight()) { |
| sizeChanged = true; |
| } |
| } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) { |
| if (mDesiredHeightAtMeasure >= 0) { |
| int desiredHeight = getDesiredHeight(); |
| |
| if (desiredHeight != mDesiredHeightAtMeasure) { |
| sizeChanged = true; |
| } |
| } |
| } |
| } |
| |
| if (sizeChanged) { |
| requestLayout(); |
| // caller will have already invalidated |
| } |
| } |
| |
| /** |
| * Check whether entirely new text requires a new view layout |
| * or merely a new text layout. |
| */ |
| private void checkForRelayout() { |
| // If we have a fixed width, we can just swap in a new text layout |
| // if the text height stays the same or if the view height is fixed. |
| |
| if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT || |
| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) && |
| (mHint == null || mHintLayout != null) && |
| (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) { |
| // Static width, so try making a new text layout. |
| |
| int oldht = mLayout.getHeight(); |
| int want = mLayout.getWidth(); |
| int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth(); |
| |
| /* |
| * No need to bring the text into view, since the size is not |
| * changing (unless we do the requestLayout(), in which case it |
| * will happen at measure). |
| */ |
| makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING, |
| mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), |
| false); |
| |
| if (mEllipsize != TextUtils.TruncateAt.MARQUEE) { |
| // In a fixed-height view, so use our new text layout. |
| if (mLayoutParams.height != LayoutParams.WRAP_CONTENT && |
| mLayoutParams.height != LayoutParams.MATCH_PARENT) { |
| invalidate(); |
| return; |
| } |
| |
| // Dynamic height, but height has stayed the same, |
| // so use our new text layout. |
| if (mLayout.getHeight() == oldht && |
| (mHintLayout == null || mHintLayout.getHeight() == oldht)) { |
| invalidate(); |
| return; |
| } |
| } |
| |
| // We lose: the height has changed and we have a dynamic height. |
| // Request a new view layout using our new text layout. |
| requestLayout(); |
| invalidate(); |
| } else { |
| // Dynamic width, so we have no choice but to request a new |
| // view layout with a new text layout. |
| |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Returns true if anything changed. |
| */ |
| private boolean bringTextIntoView() { |
| int line = 0; |
| if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { |
| line = mLayout.getLineCount() - 1; |
| } |
| |
| Layout.Alignment a = mLayout.getParagraphAlignment(line); |
| int dir = mLayout.getParagraphDirection(line); |
| int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); |
| int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); |
| int ht = mLayout.getHeight(); |
| |
| int scrollx, scrolly; |
| |
| if (a == Layout.Alignment.ALIGN_CENTER) { |
| /* |
| * Keep centered if possible, or, if it is too wide to fit, |
| * keep leading edge in view. |
| */ |
| |
| int left = (int) FloatMath.floor(mLayout.getLineLeft(line)); |
| int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); |
| |
| if (right - left < hspace) { |
| scrollx = (right + left) / 2 - hspace / 2; |
| } else { |
| if (dir < 0) { |
| scrollx = right - hspace; |
| } else { |
| scrollx = left; |
| } |
| } |
| } else if (a == Layout.Alignment.ALIGN_NORMAL) { |
| /* |
| * Keep leading edge in view. |
| */ |
| |
| if (dir < 0) { |
| int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); |
| scrollx = right - hspace; |
| } else { |
| scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); |
| } |
| } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ { |
| /* |
| * Keep trailing edge in view. |
| */ |
| |
| if (dir < 0) { |
| scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); |
| } else { |
| int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); |
| scrollx = right - hspace; |
| } |
| } |
| |
| if (ht < vspace) { |
| scrolly = 0; |
| } else { |
| if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { |
| scrolly = ht - vspace; |
| } else { |
| scrolly = 0; |
| } |
| } |
| |
| if (scrollx != mScrollX || scrolly != mScrollY) { |
| scrollTo(scrollx, scrolly); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Move the point, specified by the offset, into the view if it is needed. |
| * This has to be called after layout. Returns true if anything changed. |
| */ |
| public boolean bringPointIntoView(int offset) { |
| boolean changed = false; |
| |
| int line = mLayout.getLineForOffset(offset); |
| |
| // FIXME: Is it okay to truncate this, or should we round? |
| final int x = (int)mLayout.getPrimaryHorizontal(offset); |
| final int top = mLayout.getLineTop(line); |
| final int bottom = mLayout.getLineTop(line + 1); |
| |
| int left = (int) FloatMath.floor(mLayout.getLineLeft(line)); |
| int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); |
| int ht = mLayout.getHeight(); |
| |
| int grav; |
| |
| switch (mLayout.getParagraphAlignment(line)) { |
| case ALIGN_NORMAL: |
| grav = 1; |
| break; |
| |
| case ALIGN_OPPOSITE: |
| grav = -1; |
| break; |
| |
| default: |
| grav = 0; |
| } |
| |
| grav *= mLayout.getParagraphDirection(line); |
| |
| int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); |
| int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); |
| |
| int hslack = (bottom - top) / 2; |
| int vslack = hslack; |
| |
| if (vslack > vspace / 4) |
| vslack = vspace / 4; |
| if (hslack > hspace / 4) |
| hslack = hspace / 4; |
| |
| int hs = mScrollX; |
| int vs = mScrollY; |
| |
| if (top - vs < vslack) |
| vs = top - vslack; |
| if (bottom - vs > vspace - vslack) |
| vs = bottom - (vspace - vslack); |
| if (ht - vs < vspace) |
| vs = ht - vspace; |
| if (0 - vs > 0) |
| vs = 0; |
| |
| if (grav != 0) { |
| if (x - hs < hslack) { |
| hs = x - hslack; |
| } |
| if (x - hs > hspace - hslack) { |
| hs = x - (hspace - hslack); |
| } |
| } |
| |
| if (grav < 0) { |
| if (left - hs > 0) |
| hs = left; |
| if (right - hs < hspace) |
| hs = right - hspace; |
| } else if (grav > 0) { |
| if (right - hs < hspace) |
| hs = right - hspace; |
| if (left - hs > 0) |
| hs = left; |
| } else /* grav == 0 */ { |
| if (right - left <= hspace) { |
| /* |
| * If the entire text fits, center it exactly. |
| */ |
| hs = left - (hspace - (right - left)) / 2; |
| } else if (x > right - hslack) { |
| /* |
| * If we are near the right edge, keep the right edge |
| * at the edge of the view. |
| */ |
| hs = right - hspace; |
| } else if (x < left + hslack) { |
| /* |
| * If we are near the left edge, keep the left edge |
| * at the edge of the view. |
| */ |
| hs = left; |
| } else if (left > hs) { |
| /* |
| * Is there whitespace visible at the left? Fix it if so. |
| */ |
| hs = left; |
| } else if (right < hs + hspace) { |
| /* |
| * Is there whitespace visible at the right? Fix it if so. |
| */ |
| hs = right - hspace; |
| } else { |
| /* |
| * Otherwise, float as needed. |
| */ |
| if (x - hs < hslack) { |
| hs = x - hslack; |
| } |
| if (x - hs > hspace - hslack) { |
| hs = x - (hspace - hslack); |
| } |
| } |
| } |
| |
| if (hs != mScrollX || vs != mScrollY) { |
| if (mScroller == null) { |
| scrollTo(hs, vs); |
| } else { |
| long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; |
| int dx = hs - mScrollX; |
| int dy = vs - mScrollY; |
| |
| if (duration > ANIMATED_SCROLL_GAP) { |
| mScroller.startScroll(mScrollX, mScrollY, dx, dy); |
| 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. |
| |
| Rect r = new Rect(x, top, x + 1, bottom); |
| getInterestingRect(r, line); |
| r.offset(mScrollX, mScrollY); |
| |
| if (requestRectangleOnScreen(r)) { |
| 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); |
| |
| int newStart = start; |
| if (newStart < leftChar) { |
| newStart = leftChar; |
| } else if (newStart > rightChar) { |
| newStart = rightChar; |
| } |
| |
| 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(); |
| postInvalidate(); // So we draw again |
| } |
| } |
| } |
| |
| private void getInterestingRect(Rect r, int line) { |
| convertFromViewportToContentCoordinates(r); |
| |
| // Rectangle can can be expanded on first and last line to take |
| // padding into account. |
| // TODO Take left/right padding into account too? |
| if (line == 0) r.top -= getExtendedPaddingTop(); |
| if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom(); |
| } |
| |
| private void convertFromViewportToContentCoordinates(Rect r) { |
| final int horizontalOffset = viewportToContentHorizontalOffset(); |
| r.left += horizontalOffset; |
| r.right += horizontalOffset; |
| |
| final int verticalOffset = viewportToContentVerticalOffset(); |
| r.top += verticalOffset; |
| r.bottom += verticalOffset; |
| } |
| |
| private int viewportToContentHorizontalOffset() { |
| return getCompoundPaddingLeft() - mScrollX; |
| } |
| |
| private int viewportToContentVerticalOffset() { |
| int offset = getExtendedPaddingTop() - mScrollY; |
| if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { |
| offset += getVerticalOffset(false); |
| } |
| return offset; |
| } |
| |
| @Override |
| public void debug(int depth) { |
| super.debug(depth); |
| |
| String output = debugIndent(depth); |
| output += "frame={" + mLeft + ", " + mTop + ", " + mRight |
| + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY |
| + "} "; |
| |
| if (mText != null) { |
| |
| output += "mText=\"" + mText + "\" "; |
| if (mLayout != null) { |
| output += "mLayout width=" + mLayout.getWidth() |
| + " height=" + mLayout.getHeight(); |
| } |
| } else { |
| output += "mText=NULL"; |
| } |
| Log.d(VIEW_LOG_TAG, output); |
| } |
| |
| /** |
| * Convenience for {@link Selection#getSelectionStart}. |
| */ |
| @ViewDebug.ExportedProperty(category = "text") |
| public int getSelectionStart() { |
| return Selection.getSelectionStart(getText()); |
| } |
| |
| /** |
| * Convenience for {@link Selection#getSelectionEnd}. |
| */ |
| @ViewDebug.ExportedProperty(category = "text") |
| public int getSelectionEnd() { |
| return Selection.getSelectionEnd(getText()); |
| } |
| |
| /** |
| * Return true iff there is a selection inside this text view. |
| */ |
| public boolean hasSelection() { |
| final int selectionStart = getSelectionStart(); |
| final int selectionEnd = getSelectionEnd(); |
| |
| return selectionStart >= 0 && selectionStart != selectionEnd; |
| } |
| |
| /** |
| * Sets the properties of this field (lines, horizontally scrolling, |
| * transformation method) to be for a single-line input. |
| * |
| * @attr ref android.R.styleable#TextView_singleLine |
| */ |
| public void setSingleLine() { |
| setSingleLine(true); |
| } |
| |
| /** |
| * If true, sets the properties of this field (lines, horizontally |
| * scrolling, transformation method) to be for a single-line input; |
| * if false, restores these to the default conditions. |
| * Note that calling this with false restores default conditions, |
| * not necessarily those that were in effect prior to calling |
| * it with true. |
| * |
| * @attr ref android.R.styleable#TextView_singleLine |
| */ |
| @android.view.RemotableViewMethod |
| public void setSingleLine(boolean singleLine) { |
| if ((mInputType&EditorInfo.TYPE_MASK_CLASS) |
| == EditorInfo.TYPE_CLASS_TEXT) { |
| if (singleLine) { |
| mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; |
| } else { |
| mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; |
| } |
| } |
| applySingleLine(singleLine, true); |
| } |
| |
| private void applySingleLine(boolean singleLine, boolean applyTransformation) { |
| mSingleLine = singleLine; |
| if (singleLine) { |
| setLines(1); |
| setHorizontallyScrolling(true); |
| if (applyTransformation) { |
| setTransformationMethod(SingleLineTransformationMethod. |
| getInstance()); |
| } |
| } else { |
| setMaxLines(Integer.MAX_VALUE); |
| setHorizontallyScrolling(false); |
| if (applyTransformation) { |
| setTransformationMethod(null); |
| } |
| } |
| } |
| |
| /** |
| * Causes words in the text that are longer than the view is wide |
| * to be ellipsized instead of broken in the middle. You may also |
| * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling} |
| * to constrain the text to a single line. Use <code>null</code> |
| * to turn off ellipsizing. |
| * |
| * @attr ref android.R.styleable#TextView_ellipsize |
| */ |
| public void setEllipsize(TextUtils.TruncateAt where) { |
| mEllipsize = where; |
| |
| if (mLayout != null) { |
| nullLayouts(); |
| requestLayout(); |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Sets how many times to repeat the marquee animation. Only applied if the |
| * TextView has marquee enabled. Set to -1 to repeat indefinitely. |
| * |
| * @attr ref android.R.styleable#TextView_marqueeRepeatLimit |
| */ |
| public void setMarqueeRepeatLimit(int marqueeLimit) { |
| mMarqueeRepeatLimit = marqueeLimit; |
| } |
| |
| /** |
| * Returns where, if anywhere, words that are longer than the view |
| * is wide should be ellipsized. |
| */ |
| @ViewDebug.ExportedProperty |
| public TextUtils.TruncateAt getEllipsize() { |
| return mEllipsize; |
| } |
| |
| /** |
| * Set the TextView so that when it takes focus, all the text is |
| * selected. |
| * |
| * @attr ref android.R.styleable#TextView_selectAllOnFocus |
| */ |
| @android.view.RemotableViewMethod |
| public void setSelectAllOnFocus(boolean selectAllOnFocus) { |
| mSelectAllOnFocus = selectAllOnFocus; |
| |
| if (selectAllOnFocus && !(mText instanceof Spannable)) { |
| setText(mText, BufferType.SPANNABLE); |
| } |
| } |
| |
| /** |
| * Set whether the cursor is visible. The default is true. |
| * |
| * @attr ref android.R.styleable#TextView_cursorVisible |
| */ |
| @android.view.RemotableViewMethod |
| public void setCursorVisible(boolean visible) { |
| mCursorVisible = visible; |
| invalidate(); |
| |
| if (visible) { |
| makeBlink(); |
| } else if (mBlink != null) { |
| mBlink.removeCallbacks(mBlink); |
| } |
| |
| // InsertionPointCursorController depends on mCursorVisible |
| prepareCursorControllers(); |
| } |
| |
| private boolean canMarquee() { |
| int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()); |
| return width > 0 && mLayout.getLineWidth(0) > width; |
| } |
| |
| private void startMarquee() { |
| // Do not ellipsize EditText |
| if (mInput != null) return; |
| |
| if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) { |
| return; |
| } |
| |
| if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) && |
| getLineCount() == 1 && canMarquee()) { |
| |
| if (mMarquee == null) mMarquee = new Marquee(this); |
| mMarquee.start(mMarqueeRepeatLimit); |
| } |
| } |
| |
| private void stopMarquee() { |
| if (mMarquee != null && !mMarquee.isStopped()) { |
| mMarquee.stop(); |
| } |
| } |
| |
| private void startStopMarquee(boolean start) { |
| if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { |
| if (start) { |
| startMarquee(); |
| } else { |
| stopMarquee(); |
| } |
| } |
| } |
| |
| private static final class Marquee extends Handler { |
| // TODO: Add an option to configure this |
| private static final float MARQUEE_DELTA_MAX = 0.07f; |
| private static final int MARQUEE_DELAY = 1200; |
| private static final int MARQUEE_RESTART_DELAY = 1200; |
| private static final int MARQUEE_RESOLUTION = 1000 / 30; |
| private static final int MARQUEE_PIXELS_PER_SECOND = 30; |
| |
| private static final byte MARQUEE_STOPPED = 0x0; |
| private static final byte MARQUEE_STARTING = 0x1; |
| private static final byte MARQUEE_RUNNING = 0x2; |
| |
| private static final int MESSAGE_START = 0x1; |
| private static final int MESSAGE_TICK = 0x2; |
| private static final int MESSAGE_RESTART = 0x3; |
| |
| private final WeakReference<TextView> mView; |
| |
| private byte mStatus = MARQUEE_STOPPED; |
| private final float mScrollUnit; |
| private float mMaxScroll; |
| float mMaxFadeScroll; |
| private float mGhostStart; |
| private float mGhostOffset; |
| private float mFadeStop; |
| private int mRepeatLimit; |
| |
| float mScroll; |
| |
| Marquee(TextView v) { |
| final float density = v.getContext().getResources().getDisplayMetrics().density; |
| mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION; |
| mView = new WeakReference<TextView>(v); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MESSAGE_START: |
| mStatus = MARQUEE_RUNNING; |
| tick(); |
| break; |
| case MESSAGE_TICK: |
| tick(); |
| break; |
| case MESSAGE_RESTART: |
| if (mStatus == MARQUEE_RUNNING) { |
| if (mRepeatLimit >= 0) { |
| mRepeatLimit--; |
| } |
| start(mRepeatLimit); |
| } |
| break; |
| } |
| } |
| |
| void tick() { |
| if (mStatus != MARQUEE_RUNNING) { |
| return; |
| } |
| |
| removeMessages(MESSAGE_TICK); |
| |
| final TextView textView = mView.get(); |
| if (textView != null && (textView.isFocused() || textView.isSelected())) { |
| mScroll += mScrollUnit; |
| if (mScroll > mMaxScroll) { |
| mScroll = mMaxScroll; |
| sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY); |
| } else { |
| sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION); |
| } |
| textView.invalidate(); |
| } |
| } |
| |
| void stop() { |
| mStatus = MARQUEE_STOPPED; |
| removeMessages(MESSAGE_START); |
| removeMessages(MESSAGE_RESTART); |
| removeMessages(MESSAGE_TICK); |
| resetScroll(); |
| } |
| |
| private void resetScroll() { |
| mScroll = 0.0f; |
| final TextView textView = mView.get(); |
| if (textView != null) textView.invalidate(); |
| } |
| |
| void start(int repeatLimit) { |
| if (repeatLimit == 0) { |
| stop(); |
| return; |
| } |
| mRepeatLimit = repeatLimit; |
| final TextView textView = mView.get(); |
| if (textView != null && textView.mLayout != null) { |
| mStatus = MARQUEE_STARTING; |
| mScroll = 0.0f; |
| final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() - |
| textView.getCompoundPaddingRight(); |
| final float lineWidth = textView.mLayout.getLineWidth(0); |
| final float gap = textWidth / 3.0f; |
| mGhostStart = lineWidth - textWidth + gap; |
| mMaxScroll = mGhostStart + textWidth; |
| mGhostOffset = lineWidth + gap; |
| mFadeStop = lineWidth + textWidth / 6.0f; |
| mMaxFadeScroll = mGhostStart + lineWidth + lineWidth; |
| |
| textView.invalidate(); |
| sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY); |
| } |
| } |
| |
| float getGhostOffset() { |
| return mGhostOffset; |
| } |
| |
| boolean shouldDrawLeftFade() { |
| return mScroll <= mFadeStop; |
| } |
| |
| boolean shouldDrawGhost() { |
| return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart; |
| } |
| |
| boolean isRunning() { |
| return mStatus == MARQUEE_RUNNING; |
| } |
| |
| boolean isStopped() { |
| return mStatus == MARQUEE_STOPPED; |
| } |
| } |
| |
| /** |
| * This method is called when the text is changed, in case any |
| * subclasses would like to know. |
| * |
| * @param text The text the TextView is displaying. |
| * @param start The offset of the start of the range of the text |
| * that was modified. |
| * @param before The offset of the former end of the range of the |
| * text that was modified. If text was simply inserted, |
| * this will be the same as <code>start</code>. |
| * If text was replaced with new text or deleted, the |
| * length of the old text was <code>before-start</code>. |
| * @param after The offset of the end of the range of the text |
| * that was modified. If text was simply deleted, |
| * this will be the same as <code>start</code>. |
| * If text was replaced with new text or inserted, |
| * the length of the new text is <code>after-start</code>. |
| */ |
| protected void onTextChanged(CharSequence text, |
| int start, int before, int after) { |
| } |
| |
| /** |
| * This method is called when the selection has changed, in case any |
| * subclasses would like to know. |
| * |
| * @param selStart The new selection start location. |
| * @param selEnd The new selection end location. |
| */ |
| protected void onSelectionChanged(int selStart, int selEnd) { |
| } |
| |
| /** |
| * Adds a TextWatcher to the list of those whose methods are called |
| * whenever this TextView's text changes. |
| * <p> |
| * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously |
| * not called after {@link #setText} calls. Now, doing {@link #setText} |
| * if there are any text changed listeners forces the buffer type to |
| * Editable if it would not otherwise be and does call this method. |
| */ |
| public void addTextChangedListener(TextWatcher watcher) { |
| if (mListeners == null) { |
| mListeners = new ArrayList<TextWatcher>(); |
| } |
| |
| mListeners.add(watcher); |
| } |
| |
| /** |
| * Removes the specified TextWatcher from the list of those whose |
| * methods are called |
| * whenever this TextView's text changes. |
| */ |
| public void removeTextChangedListener(TextWatcher watcher) { |
| if (mListeners != null) { |
| int i = mListeners.indexOf(watcher); |
| |
| if (i >= 0) { |
| mListeners.remove(i); |
| } |
| } |
| } |
| |
| private void sendBeforeTextChanged(CharSequence text, int start, int before, |
| int after) { |
| if (mListeners != null) { |
| final ArrayList<TextWatcher> list = mListeners; |
| final int count = list.size(); |
| for (int i = 0; i < count; i++) { |
| list.get(i).beforeTextChanged(text, start, before, after); |
| } |
| } |
| } |
| |
| /** |
| * Not private so it can be called from an inner class without going |
| * through a thunk. |
| */ |
| void sendOnTextChanged(CharSequence text, int start, int before, |
| int after) { |
| if (mListeners != null) { |
| final ArrayList<TextWatcher> list = mListeners; |
| final int count = list.size(); |
| for (int i = 0; i < count; i++) { |
| list.get(i).onTextChanged(text, start, before, after); |
| } |
| } |
| } |
| |
| /** |
| * Not private so it can be called from an inner class without going |
| * through a thunk. |
| */ |
| void sendAfterTextChanged(Editable text) { |
| if (mListeners != null) { |
| final ArrayList<TextWatcher> list = mListeners; |
| final int count = list.size(); |
| for (int i = 0; i < count; i++) { |
| list.get(i).afterTextChanged(text); |
| } |
| } |
| } |
| |
| /** |
| * Not private so it can be called from an inner class without going |
| * through a thunk. |
| */ |
| void handleTextChanged(CharSequence buffer, int start, |
| int before, int after) { |
| final InputMethodState ims = mInputMethodState; |
| if (ims == null || ims.mBatchEditNesting == 0) { |
| updateAfterEdit(); |
| } |
| if (ims != null) { |
| ims.mContentChanged = true; |
| if (ims.mChangedStart < 0) { |
| ims.mChangedStart = start; |
| ims.mChangedEnd = start+before; |
| } else { |
| if (ims.mChangedStart > start) ims.mChangedStart = start; |
| if (ims.mChangedEnd < (start+before)) ims.mChangedEnd = start+before; |
| } |
| ims.mChangedDelta += after-before; |
| } |
| |
| sendOnTextChanged(buffer, start, before, after); |
| onTextChanged(buffer, start, before, after); |
| hideControllers(); |
| } |
| |
| /** |
| * Not private so it can be called from an inner class without going |
| * through a thunk. |
| */ |
| void spanChange(Spanned buf, Object what, int oldStart, int newStart, |
| int oldEnd, int newEnd) { |
| // XXX Make the start and end move together if this ends up |
| // spending too much time invalidating. |
| |
| boolean selChanged = false; |
| int newSelStart=-1, newSelEnd=-1; |
| |
| final InputMethodState ims = mInputMethodState; |
| |
| if (what == Selection.SELECTION_END) { |
| mHighlightPathBogus = true; |
| selChanged = true; |
| newSelEnd = newStart; |
| |
| if (!isFocused()) { |
| mSelectionMoved = true; |
| } |
| |
| if (oldStart >= 0 || newStart >= 0) { |
| invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart); |
| registerForPreDraw(); |
| |
| if (isFocused()) { |
| mShowCursor = SystemClock.uptimeMillis(); |
| makeBlink(); |
| } |
| } |
| } |
| |
| if (what == Selection.SELECTION_START) { |
| mHighlightPathBogus = true; |
| selChanged = true; |
| newSelStart = newStart; |
| |
| if (!isFocused()) { |
| mSelectionMoved = true; |
| } |
| |
| if (oldStart >= 0 || newStart >= 0) { |
| int end = Selection.getSelectionEnd(buf); |
| invalidateCursor(end, oldStart, newStart); |
| } |
| } |
| |
| if (selChanged) { |
| if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) { |
| if (newSelStart < 0) { |
| newSelStart = Selection.getSelectionStart(buf); |
| } |
| if (newSelEnd < 0) { |
| newSelEnd = Selection.getSelectionEnd(buf); |
| } |
| onSelectionChanged(newSelStart, newSelEnd); |
| } |
| } |
| |
| if (what instanceof UpdateAppearance || |
| what instanceof ParagraphStyle) { |
| if (ims == null || ims.mBatchEditNesting == 0) { |
| invalidate(); |
| mHighlightPathBogus = true; |
| checkForResize(); |
| } else { |
| ims.mContentChanged = true; |
| } |
| } |
| |
| if (MetaKeyKeyListener.isMetaTracker(buf, what)) { |
| mHighlightPathBogus = true; |
| if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) { |
| ims.mSelectionModeChanged = true; |
| } |
| |
| if (Selection.getSelectionStart(buf) >= 0) { |
| if (ims == null || ims.mBatchEditNesting == 0) { |
| invalidateCursor(); |
| } else { |
| ims.mCursorChanged = true; |
| } |
| } |
| } |
| |
| if (what instanceof ParcelableSpan) { |
| // If this is a span that can be sent to a remote process, |
| // the current extract editor would be interested in it. |
| if (ims != null && ims.mExtracting != null) { |
| if (ims.mBatchEditNesting != 0) { |
| if (oldStart >= 0) { |
| if (ims.mChangedStart > oldStart) { |
| ims.mChangedStart = oldStart; |
| } |
| if (ims.mChangedStart > oldEnd) { |
| ims.mChangedStart = oldEnd; |
| } |
| } |
| if (newStart >= 0) { |
| if (ims.mChangedStart > newStart) { |
| ims.mChangedStart = newStart; |
| } |
| if (ims.mChangedStart > newEnd) { |
| ims.mChangedStart = newEnd; |
| } |
| } |
| } else { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: " |
| + oldStart + "-" + oldEnd + "," |
| + newStart + "-" + newEnd + what); |
| ims.mContentChanged = true; |
| } |
| } |
| } |
| } |
| |
| private class ChangeWatcher |
| implements TextWatcher, SpanWatcher { |
| |
| private CharSequence mBeforeText; |
| |
| public void beforeTextChanged(CharSequence buffer, int start, |
| int before, int after) { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start |
| + " before=" + before + " after=" + after + ": " + buffer); |
| |
| if (AccessibilityManager.getInstance(mContext).isEnabled() |
| && !isPasswordInputType(mInputType)) { |
| mBeforeText = buffer.toString(); |
| } |
| |
| TextView.this.sendBeforeTextChanged(buffer, start, before, after); |
| } |
| |
| public void onTextChanged(CharSequence buffer, int start, |
| int before, int after) { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start |
| + " before=" + before + " after=" + after + ": " + buffer); |
| TextView.this.handleTextChanged(buffer, start, before, after); |
| |
| if (AccessibilityManager.getInstance(mContext).isEnabled() && |
| (isFocused() || isSelected() && |
| isShown())) { |
| sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after); |
| mBeforeText = null; |
| } |
| } |
| |
| public void afterTextChanged(Editable buffer) { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer); |
| TextView.this.sendAfterTextChanged(buffer); |
| |
| if (MetaKeyKeyListener.getMetaState(buffer, |
| MetaKeyKeyListener.META_SELECTING) != 0) { |
| MetaKeyKeyListener.stopSelecting(TextView.this, buffer); |
| } |
| } |
| |
| public void onSpanChanged(Spannable buf, |
| Object what, int s, int e, int st, int en) { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e |
| + " st=" + st + " en=" + en + " what=" + what + ": " + buf); |
| TextView.this.spanChange(buf, what, s, st, e, en); |
| } |
| |
| public void onSpanAdded(Spannable buf, Object what, int s, int e) { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e |
| + " what=" + what + ": " + buf); |
| TextView.this.spanChange(buf, what, -1, s, -1, e); |
| } |
| |
| public void onSpanRemoved(Spannable buf, Object what, int s, int e) { |
| if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e |
| + " what=" + what + ": " + buf); |
| TextView.this.spanChange(buf, what, s, -1, e, -1); |
| } |
| } |
| |
| private void makeBlink() { |
| if (!mCursorVisible) { |
| if (mBlink != null) { |
| mBlink.removeCallbacks(mBlink); |
| } |
| |
| return; |
| } |
| |
| if (mBlink == null) |
| mBlink = new Blink(this); |
| |
| mBlink.removeCallbacks(mBlink); |
| mBlink.postAtTime(mBlink, mShowCursor + BLINK); |
| } |
| |
| /** |
| * @hide |
| */ |
| @Override |
| public void dispatchFinishTemporaryDetach() { |
| mDispatchTemporaryDetach = true; |
| super.dispatchFinishTemporaryDetach(); |
| mDispatchTemporaryDetach = false; |
| } |
| |
| @Override |
| public void onStartTemporaryDetach() { |
| super.onStartTemporaryDetach(); |
| // Only track when onStartTemporaryDetach() is called directly, |
| // usually because this instance is an editable field in a list |
| if (!mDispatchTemporaryDetach) mTemporaryDetach = true; |
| } |
| |
| @Override |
| public void onFinishTemporaryDetach() { |
| super.onFinishTemporaryDetach(); |
| // Only track when onStartTemporaryDetach() is called directly, |
| // usually because this instance is an editable field in a list |
| if (!mDispatchTemporaryDetach) mTemporaryDetach = false; |
| } |
| |
| @Override |
| protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { |
| if (mTemporaryDetach) { |
| // If we are temporarily in the detach state, then do nothing. |
| super.onFocusChanged(focused, direction, previouslyFocusedRect); |
| return; |
| } |
| |
| mShowCursor = SystemClock.uptimeMillis(); |
| |
| ensureEndedBatchEdit(); |
| |
| if (focused) { |
| int selStart = getSelectionStart(); |
| int selEnd = getSelectionEnd(); |
| |
| if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) { |
| // Has to be done before onTakeFocus, which can be overloaded. |
| if (mLastTouchOffset >= 0) { |
| // Can happen when a TextView is displayed after its content has been deleted. |
| mLastTouchOffset = Math.min(mLastTouchOffset, mText.length()); |
| Selection.setSelection((Spannable) mText, mLastTouchOffset); |
| } |
| |
| if (mMovement != null) { |
| mMovement.onTakeFocus(this, (Spannable) mText, direction); |
| } |
| |
| if (mSelectAllOnFocus) { |
| Selection.setSelection((Spannable) mText, 0, mText.length()); |
| } |
| |
| // The DecorView does not have focus when the 'Done' ExtractEditText button is |
| // pressed. Since it is the ViewRoot's mView, it requests focus before |
| // ExtractEditText clears focus, which gives focus to the ExtractEditText. |
| // This special case ensure that we keep current selection in that case. |
| // It would be better to know why the DecorView does not have focus at that time. |
| if (((this instanceof ExtractEditText) || mSelectionMoved) && |
| selStart >= 0 && selEnd >= 0) { |
| /* |
| * Someone intentionally set the selection, so let them |
| * do whatever it is that they wanted to do instead of |
| * the default on-focus behavior. We reset the selection |
| * here instead of just skipping the onTakeFocus() call |
| * because some movement methods do something other than |
| * just setting the selection in theirs and we still |
| * need to go through that path. |
| */ |
| Selection.setSelection((Spannable) mText, selStart, selEnd); |
| } |
| mTouchFocusSelected = true; |
| } |
| |
| mFrozenWithFocus = false; |
| mSelectionMoved = false; |
| |
| if (mText instanceof Spannable) { |
| Spannable sp = (Spannable) mText; |
| MetaKeyKeyListener.resetMetaState(sp); |
| } |
| |
| makeBlink(); |
| |
| if (mError != null) { |
| showError(); |
| } |
| } else { |
| if (mError != null) { |
| hideError(); |
| } |
| // Don't leave us in the middle of a batch edit. |
| onEndBatchEdit(); |
| |
| hideInsertionPointCursorController(); |
| if (this instanceof ExtractEditText) { |
| // terminateTextSelectionMode would remove selection, which we want to keep when |
| // ExtractEditText goes out of focus. |
| mIsInTextSelectionMode = false; |
| } else { |
| terminateTextSelectionMode(); |
| } |
| } |
| |
| startStopMarquee(focused); |
| |
| if (mTransformation != null) { |
| mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect); |
| } |
| |
| super.onFocusChanged(focused, direction, previouslyFocusedRect); |
| } |
| |
| @Override |
| public void onWindowFocusChanged(boolean hasWindowFocus) { |
| super.onWindowFocusChanged(hasWindowFocus); |
| |
| if (hasWindowFocus) { |
| if (mBlink != null) { |
| mBlink.uncancel(); |
| |
| if (isFocused()) { |
| mShowCursor = SystemClock.uptimeMillis(); |
| makeBlink(); |
| } |
| } |
| } else { |
| if (mBlink != null) { |
| mBlink.cancel(); |
| } |
| // Don't leave us in the middle of a batch edit. |
| onEndBatchEdit(); |
| if (mInputContentType != null) { |
| mInputContentType.enterDown = false; |
| } |
| } |
| |
| startStopMarquee(hasWindowFocus); |
| } |
| |
| /** |
| * Use {@link BaseInputConnection#removeComposingSpans |
| * BaseInputConnection.removeComposingSpans()} to remove any IME composing |
| * state from this text view. |
| */ |
| public void clearComposingText() { |
| if (mText instanceof Spannable) { |
| BaseInputConnection.removeComposingSpans((Spannable)mText); |
| } |
| } |
| |
| @Override |
| public void setSelected(boolean selected) { |
| boolean wasSelected = isSelected(); |
| |
| super.setSelected(selected); |
| |
| if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) { |
| if (selected) { |
| startMarquee(); |
| } else { |
| stopMarquee(); |
| } |
| } |
| } |
| |
| private void onTapUpEvent(int prevStart, int prevEnd) { |
| final int start = getSelectionStart(); |
| final int end = getSelectionEnd(); |
| |
| if (start == end) { |
| if (start >= prevStart && start < prevEnd) { |
| // Restore previous selection |
| Selection.setSelection((Spannable)mText, prevStart, prevEnd); |
| // Tapping inside the selection displays the cut/copy/paste context menu. |
| showContextMenu(); |
| return; |
| } else { |
| // Tapping outside stops selection mode, if any |
| stopTextSelectionMode(); |
| |
| if (mInsertionPointCursorController != null) { |
| mInsertionPointCursorController.show(); |
| } |
| } |
| } |
| } |
| |
| class CommitSelectionReceiver extends ResultReceiver { |
| private final int mPrevStart, mPrevEnd; |
| |
| public CommitSelectionReceiver(int prevStart, int prevEnd) { |
| super(getHandler()); |
| mPrevStart = prevStart; |
| mPrevEnd = prevEnd; |
| } |
| |
| @Override |
| protected void onReceiveResult(int resultCode, Bundle resultData) { |
| // If this tap was actually used to show the IMM, leave cursor or selection unchanged |
| // by restoring its previous position. |
| if (resultCode == InputMethodManager.RESULT_SHOWN) { |
| final int len = mText.length(); |
| int start = Math.min(len, mPrevStart); |
| int end = Math.min(len, mPrevEnd); |
| Selection.setSelection((Spannable)mText, start, end); |
| |
| if (hasSelection()) { |
| startTextSelectionMode(); |
| } else if (mInsertionPointCursorController != null) { |
| mInsertionPointCursorController.show(); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| final int action = event.getActionMasked(); |
| if (action == MotionEvent.ACTION_DOWN) { |
| // Reset this state; it will be re-set if super.onTouchEvent |
| // causes focus to move to the view. |
| mTouchFocusSelected = false; |
| mScrolled = false; |
| } |
| |
| final boolean superResult = super.onTouchEvent(event); |
| |
| /* |
| * Don't handle the release after a long press, because it will |
| * move the selection away from whatever the menu action was |
| * trying to affect. |
| */ |
| if (mEatTouchRelease && action == MotionEvent.ACTION_UP) { |
| mEatTouchRelease = false; |
| return superResult; |
| } |
| |
| if ((mMovement != null || onCheckIsTextEditor()) && mText instanceof Spannable && mLayout != null) { |
| |
| if (mInsertionPointCursorController != null) { |
| mInsertionPointCursorController.onTouchEvent(event); |
| } |
| if (mSelectionModifierCursorController != null) { |
| mSelectionModifierCursorController.onTouchEvent(event); |
| } |
| |
| boolean handled = false; |
| |
| // Save previous selection, in case this event is used to show the IME. |
| int oldSelStart = getSelectionStart(); |
| int oldSelEnd = getSelectionEnd(); |
| |
| if (mMovement != null) { |
| handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); |
| } |
| |
| if (isTextEditable()) { |
| if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) { |
| InputMethodManager imm = (InputMethodManager) |
| getContext().getSystemService(Context.INPUT_METHOD_SERVICE); |
| |
| CommitSelectionReceiver csr = null; |
| if (getSelectionStart() != oldSelStart || getSelectionEnd() != oldSelEnd || |
| didTouchFocusSelect()) { |
| csr = new CommitSelectionReceiver(oldSelStart, oldSelEnd); |
| } |
| |
| handled |= imm.showSoftInput(this, 0, csr) && (csr != null); |
| |
| // Cannot be done by CommitSelectionReceiver, which might not always be called, |
| // for instance when dealing with an ExtractEditText. |
| onTapUpEvent(oldSelStart, oldSelEnd); |
| } |
| } |
| |
| if (handled) { |
| return true; |
| } |
| } |
| |
| return superResult; |
| } |
| |
| private void prepareCursorControllers() { |
| // TODO Add an extra android:cursorController flag to disable the controller? |
| if (mCursorVisible && mLayout != null) { |
| if (mInsertionPointCursorController == null) { |
| mInsertionPointCursorController = new InsertionPointCursorController(); |
| } |
| } else { |
| mInsertionPointCursorController = null; |
| } |
| |
| if (canSelectText() && mLayout != null) { |
| if (mSelectionModifierCursorController == null) { |
| mSelectionModifierCursorController = new SelectionModifierCursorController(); |
| } |
| } else { |
| // Stop selection mode if the controller becomes unavailable. |
| stopTextSelectionMode(); |
| mSelectionModifierCursorController = null; |
| } |
| } |
| |
| /** |
| * @return True iff this TextView contains a text that can be edited. |
| */ |
| private boolean isTextEditable() { |
| return mText instanceof Editable && onCheckIsTextEditor(); |
| } |
| |
| /** |
| * Returns true, only while processing a touch gesture, if the initial |
| * touch down event caused focus to move to the text view and as a result |
| * its selection changed. Only valid while processing the touch gesture |
| * of interest. |
| */ |
| public boolean didTouchFocusSelect() { |
| return mTouchFocusSelected; |
| } |
| |
| @Override |
| public void cancelLongPress() { |
| super.cancelLongPress(); |
| mScrolled = true; |
| } |
| |
| @Override |
| public boolean onTrackballEvent(MotionEvent event) { |
| if (mMovement != null && mText instanceof Spannable && |
| mLayout != null) { |
| if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) { |
| return true; |
| } |
| } |
| |
| return super.onTrackballEvent(event); |
| } |
| |
| public void setScroller(Scroller s) { |
| mScroller = s; |
| } |
| |
| private static class Blink extends Handler implements Runnable { |
| private final WeakReference<TextView> mView; |
| private boolean mCancelled; |
| |
| public Blink(TextView v) { |
| mView = new WeakReference<TextView>(v); |
| } |
| |
| public void run() { |
| if (mCancelled) { |
| return; |
| } |
| |
| removeCallbacks(Blink.this); |
| |
| TextView tv = mView.get(); |
| |
| if (tv != null && tv.isFocused()) { |
| int st = tv.getSelectionStart(); |
| int en = tv.getSelectionEnd(); |
| |
| if (st == en && st >= 0 && en >= 0) { |
| if (tv.mLayout != null) { |
| tv.invalidateCursorPath(); |
| } |
| |
| postAtTime(this, SystemClock.uptimeMillis() + BLINK); |
| } |
| } |
| } |
| |
| void cancel() { |
| if (!mCancelled) { |
| removeCallbacks(Blink.this); |
| mCancelled = true; |
| } |
| } |
| |
| void uncancel() { |
| mCancelled = false; |
| } |
| } |
| |
| @Override |
| protected float getLeftFadingEdgeStrength() { |
| if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { |
| if (mMarquee != null && !mMarquee.isStopped()) { |
| final Marquee marquee = mMarquee; |
| if (marquee.shouldDrawLeftFade()) { |
| return marquee.mScroll / getHorizontalFadingEdgeLength(); |
| } else { |
| return 0.0f; |
| } |
| } else if (getLineCount() == 1) { |
| switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { |
| case Gravity.LEFT: |
| return 0.0f; |
| case Gravity.RIGHT: |
| return (mLayout.getLineRight(0) - (mRight - mLeft) - |
| getCompoundPaddingLeft() - getCompoundPaddingRight() - |
| mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength(); |
| case Gravity.CENTER_HORIZONTAL: |
| return 0.0f; |
| } |
| } |
| } |
| return super.getLeftFadingEdgeStrength(); |
| } |
| |
| @Override |
| protected float getRightFadingEdgeStrength() { |
| if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { |
| if (mMarquee != null && !mMarquee.isStopped()) { |
| final Marquee marquee = mMarquee; |
| return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength(); |
| } else if (getLineCount() == 1) { |
| switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { |
| case Gravity.LEFT: |
| final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() - |
| getCompoundPaddingRight(); |
| final float lineWidth = mLayout.getLineWidth(0); |
| return (lineWidth - textWidth) / getHorizontalFadingEdgeLength(); |
| case Gravity.RIGHT: |
| return 0.0f; |
| case Gravity.CENTER_HORIZONTAL: |
| return (mLayout.getLineWidth(0) - ((mRight - mLeft) - |
| getCompoundPaddingLeft() - getCompoundPaddingRight())) / |
| getHorizontalFadingEdgeLength(); |
| } |
| } |
| } |
| return super.getRightFadingEdgeStrength(); |
| } |
| |
| @Override |
| protected int computeHorizontalScrollRange() { |
| if (mLayout != null) |
| return mLayout.getWidth(); |
| |
| return super.computeHorizontalScrollRange(); |
| } |
| |
| @Override |
| protected int computeVerticalScrollRange() { |
| if (mLayout != null) |
| return mLayout.getHeight(); |
| |
| return super.computeVerticalScrollRange(); |
| } |
| |
| @Override |
| protected int computeVerticalScrollExtent() { |
| return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); |
| } |
| |
| public enum BufferType { |
| NORMAL, SPANNABLE, EDITABLE, |
| } |
| |
| /** |
| * Returns the TextView_textColor attribute from the |
| * Resources.StyledAttributes, if set, or the TextAppearance_textColor |
| * from the TextView_textAppearance attribute, if TextView_textColor |
| * was not set directly. |
| */ |
| public static ColorStateList getTextColors(Context context, TypedArray attrs) { |
| ColorStateList colors; |
| colors = attrs.getColorStateList(com.android.internal.R.styleable. |
| TextView_textColor); |
| |
| if (colors == null) { |
| int ap = attrs.getResourceId(com.android.internal.R.styleable. |
| TextView_textAppearance, -1); |
| if (ap != -1) { |
| TypedArray appearance; |
| appearance = context.obtainStyledAttributes(ap, |
| com.android.internal.R.styleable.TextAppearance); |
| colors = appearance.getColorStateList(com.android.internal.R.styleable. |
| TextAppearance_textColor); |
| appearance.recycle(); |
| } |
| } |
| |
| return colors; |
| } |
| |
| /** |
| * Returns the default color from the TextView_textColor attribute |
| * from the AttributeSet, if set, or the default color from the |
| * TextAppearance_textColor from the TextView_textAppearance attribute, |
| * if TextView_textColor was not set directly. |
| */ |
| public static int getTextColor(Context context, |
| TypedArray attrs, |
| int def) { |
| ColorStateList colors = getTextColors(context, attrs); |
| |
| if (colors == null) { |
| return def; |
| } else { |
| return colors.getDefaultColor(); |
| } |
| } |
| |
| @Override |
| public boolean onKeyShortcut(int keyCode, KeyEvent event) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_A: |
| if (canSelectAll()) { |
| return onTextContextMenuItem(ID_SELECT_ALL); |
| } |
| |
| break; |
| |
| case KeyEvent.KEYCODE_X: |
| if (canCut()) { |
| return onTextContextMenuItem(ID_CUT); |
| } |
| |
| break; |
| |
| case KeyEvent.KEYCODE_C: |
| if (canCopy()) { |
| return onTextContextMenuItem(ID_COPY); |
| } |
| |
| break; |
| |
| case KeyEvent.KEYCODE_V: |
| if (canPaste()) { |
| return onTextContextMenuItem(ID_PASTE); |
| } |
| |
| break; |
| } |
| |
| return super.onKeyShortcut(keyCode, event); |
| } |
| |
| private boolean canSelectAll() { |
| return canSelectText() && mText.length() != 0; |
| } |
| |
| private boolean canSelectText() { |
| // prepareCursorController() relies on this method. |
| // If you change this condition, make sure prepareCursorController is called anywhere |
| // the value of this condition might be changed. |
| return (mText instanceof Spannable && |
| mMovement != null && |
| mMovement.canSelectArbitrarily()); |
| } |
| |
| private boolean canCut() { |
| if (mTransformation instanceof PasswordTransformationMethod) { |
| return false; |
| } |
| |
| if (mText.length() > 0 && hasSelection()) { |
| if (mText instanceof Editable && mInput != null) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private boolean canCopy() { |
| if (mTransformation instanceof PasswordTransformationMethod) { |
| return false; |
| } |
| |
| if (mText.length() > 0 && hasSelection()) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean canPaste() { |
| return (mText instanceof Editable && |
| mInput != null && |
| getSelectionStart() >= 0 && |
| getSelectionEnd() >= 0 && |
| ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)). |
| hasText()); |
| } |
| |
| /** |
| * Returns the offsets delimiting the 'word' located at position offset. |
| * |
| * @param offset An offset in the text. |
| * @return The offsets for the start and end of the word located at <code>offset</code>. |
| * The two ints offsets are packed in a long, with the starting offset shifted by 32 bits. |
| * Returns a negative value if no valid word was found. |
| */ |
| private long getWordLimitsAt(int offset) { |
| /* |
| * Quick return if the input type is one where adding words |
| * to the dictionary doesn't make any sense. |
| */ |
| int klass = mInputType & InputType.TYPE_MASK_CLASS; |
| if (klass == InputType.TYPE_CLASS_NUMBER || |
| klass == InputType.TYPE_CLASS_PHONE || |
| klass == InputType.TYPE_CLASS_DATETIME) { |
| return -1; |
| } |
| |
| int variation = mInputType & InputType.TYPE_MASK_VARIATION; |
| if (variation == InputType.TYPE_TEXT_VARIATION_URI || |
| variation == InputType.TYPE_TEXT_VARIATION_PASSWORD || |
| variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD || |
| variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || |
| variation == InputType.TYPE_TEXT_VARIATION_FILTER) { |
| return -1; |
| } |
| |
| int len = mText.length(); |
| int end = Math.min(offset, len); |
| |
| if (end < 0) { |
| return -1; |
| } |
| |
| int start = end; |
| |
| for (; start > 0; start--) { |
| char c = mTransformed.charAt(start - 1); |
| int type = Character.getType(c); |
| |
| if (c != '\'' && |
| type != Character.UPPERCASE_LETTER && |
| type != Character.LOWERCASE_LETTER && |
| type != Character.TITLECASE_LETTER && |
| type != Character.MODIFIER_LETTER && |
| type != Character.DECIMAL_DIGIT_NUMBER) { |
| break; |
| } |
| } |
| |
| for (; end < len; end++) { |
| char c = mTransformed.charAt(end); |
| int type = Character.getType(c); |
| |
| if (c != '\'' && |
| type != Character.UPPERCASE_LETTER && |
| type != Character.LOWERCASE_LETTER && |
| type != Character.TITLECASE_LETTER && |
| type != Character.MODIFIER_LETTER && |
| type != Character.DECIMAL_DIGIT_NUMBER) { |
| break; |
| } |
| } |
| |
| if (start == end) { |
| return -1; |
| } |
| |
| if (end - start > 48) { |
| return -1; |
| } |
| |
| boolean hasLetter = false; |
| for (int i = start; i < end; i++) { |
| if (Character.isLetter(mTransformed.charAt(i))) { |
| hasLetter = true; |
| break; |
| } |
| } |
| |
| if (!hasLetter) { |
| return -1; |
| } |
| |
| // Two ints packed in a long |
| return (((long) start) << 32) | end; |
| } |
| |
| private void selectCurrentWord() { |
| // In case selection mode is started after an orientation change or after a select all, |
| // use the current selection instead of creating one |
| if (hasSelection()) { |
| return; |
| } |
| |
| int selectionStart, selectionEnd; |
| |
| // selectionModifierCursorController is not null at that point |
| SelectionModifierCursorController selectionModifierCursorController = |
| ((SelectionModifierCursorController) mSelectionModifierCursorController); |
| int minOffset = selectionModifierCursorController.getMinTouchOffset(); |
| int maxOffset = selectionModifierCursorController.getMaxTouchOffset(); |
| |
| if (minOffset == maxOffset) { |
| int offset = Math.max(0, Math.min(minOffset, mTransformed.length())); |
| |
| // Tolerance, number of charaters around tapped position |
| final int range = 1; |
| final int max = mTransformed.length() - 1; |
| |
| // 'Smart' word selection: detect position between words |
| for (int i = -range; i <= range; i++) { |
| int index = offset + i; |
| if (index >= 0 && index <= max) { |
| if (Character.isSpaceChar(mTransformed.charAt(index))) { |
| // Select current space |
| selectionStart = index; |
| selectionEnd = selectionStart + 1; |
| |
| // Extend selection to maximum space range |
| while (selectionStart > 0 && |
| Character.isSpaceChar(mTransformed.charAt(selectionStart - 1))) { |
| selectionStart--; |
| } |
| while (selectionEnd < max && |
| Character.isSpaceChar(mTransformed.charAt(selectionEnd))) { |
| selectionEnd++; |
| } |
| |
| Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); |
| return; |
| } |
| } |
| } |
| |
| // 'Smart' word selection: detect position at beginning or end of text. |
| if (offset <= range) { |
| Selection.setSelection((Spannable) mText, 0, 0); |
| return; |
| } |
| if (offset >= (max - range)) { |
| Selection.setSelection((Spannable) mText, max + 1, max + 1); |
| return; |
| } |
| } |
| |
| long wordLimits = getWordLimitsAt(minOffset); |
| if (wordLimits >= 0) { |
| selectionStart = (int) (wordLimits >>> 32); |
| } else { |
| selectionStart = Math.max(minOffset - 5, 0); |
| } |
| |
| wordLimits = getWordLimitsAt(maxOffset); |
| if (wordLimits >= 0) { |
| selectionEnd = (int) (wordLimits & 0x00000000FFFFFFFFL); |
| } else { |
| selectionEnd = Math.min(maxOffset + 5, mText.length()); |
| } |
| |
| Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); |
| } |
| |
| private String getWordForDictionary() { |
| if (mLastTouchOffset < 0) { |
| return null; |
| } |
| |
| long wordLimits = getWordLimitsAt(mLastTouchOffset); |
| if (wordLimits >= 0) { |
| int start = (int) (wordLimits >>> 32); |
| int end = (int) (wordLimits & 0x00000000FFFFFFFFL); |
| return mTransformed.subSequence(start, end).toString(); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
| if (!isShown()) { |
| return false; |
| } |
| |
| final boolean isPassword = isPasswordInputType(mInputType); |
| |
| if (!isPassword) { |
| CharSequence text = getText(); |
| if (TextUtils.isEmpty(text)) { |
| text = getHint(); |
| } |
| if (!TextUtils.isEmpty(text)) { |
| if (text.length() > AccessibilityEvent.MAX_TEXT_LENGTH) { |
| text = text.subSequence(0, AccessibilityEvent.MAX_TEXT_LENGTH + 1); |
| } |
| event.getText().add(text); |
| } |
| } else { |
| event.setPassword(isPassword); |
| } |
| return false; |
| } |
| |
| void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, |
| int fromIndex, int removedCount, int addedCount) { |
| AccessibilityEvent event = |
| AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); |
| event.setFromIndex(fromIndex); |
| event.setRemovedCount(removedCount); |
| event.setAddedCount(addedCount); |
| event.setBeforeText(beforeText); |
| sendAccessibilityEventUnchecked(event); |
| } |
| |
| @Override |
| protected void onCreateContextMenu(ContextMenu menu) { |
| super.onCreateContextMenu(menu); |
| boolean added = false; |
| |
| if (mIsInTextSelectionMode) { |
| MenuHandler handler = new MenuHandler(); |
| |
| if (canCut()) { |
| menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut). |
| setOnMenuItemClickListener(handler). |
| setAlphabeticShortcut('x'); |
| added = true; |
| } |
| |
| if (canCopy()) { |
| menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy). |
| setOnMenuItemClickListener(handler). |
| setAlphabeticShortcut('c'); |
| added = true; |
| } |
| |
| if (canPaste()) { |
| menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). |
| setOnMenuItemClickListener(handler). |
| setAlphabeticShortcut('v'); |
| added = true; |
| } |
| } else { |
| /* |
| if (!isFocused()) { |
| if (isFocusable() && mInput != null) { |
| if (canCopy()) { |
| MenuHandler handler = new MenuHandler(); |
| menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy). |
| setOnMenuItemClickListener(handler). |
| setAlphabeticShortcut('c'); |
| menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle); |
| } |
| } |
| |
| //return; |
| } |
| */ |
| MenuHandler handler = new MenuHandler(); |
| |
| if (canSelectText()) { |
| menu.add(0, ID_START_SELECTING_TEXT, 0, com.android.internal.R.string.selectText). |
| setOnMenuItemClickListener(handler); |
| added = true; |
| } |
| |
| if (canSelectAll()) { |
| menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). |
| setOnMenuItemClickListener(handler). |
| setAlphabeticShortcut('a'); |
| added = true; |
| } |
| |
| if (mText instanceof Spanned) { |
| int selStart = getSelectionStart(); |
| int selEnd = getSelectionEnd(); |
| |
| int min = Math.min(selStart, selEnd); |
| int max = Math.max(selStart, selEnd); |
| |
| URLSpan[] urls = ((Spanned) mText).getSpans(min, max, |
| URLSpan.class); |
| if (urls.length == 1) { |
| menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl). |
| setOnMenuItemClickListener(handler); |
| added = true; |
| } |
| } |
| |
| // Paste location is too imprecise. Only allow on empty text fields. |
| if (canPaste() && textIsOnlySpaces()) { |
| menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). |
| setOnMenuItemClickListener(handler). |
| setAlphabeticShortcut('v'); |
| added = true; |
| } |
| |
| if (isInputMethodTarget()) { |
| menu.add(1, ID_SWITCH_INPUT_METHOD, 0, com.android.internal.R.string.inputMethod). |
| setOnMenuItemClickListener(handler); |
| added = true; |
| } |
| |
| String word = getWordForDictionary(); |
| if (word != null) { |
| menu.add(1, ID_ADD_TO_DICTIONARY, 0, |
| getContext().getString(com.android.internal.R.string.addToDictionary, word)). |
| setOnMenuItemClickListener(handler); |
| added = true; |
| |
| } |
| } |
| |
| if (added) { |
| menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle); |
| } |
| } |
| |
| private boolean textIsOnlySpaces() { |
| final int length = mTransformed.length(); |
| for (int i = 0; i < length; i++) { |
| if (!Character.isSpaceChar(mTransformed.charAt(i))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Returns whether this text view is a current input method target. The |
| * default implementation just checks with {@link InputMethodManager}. |
| */ |
| public boolean isInputMethodTarget() { |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| return imm != null && imm.isActive(this); |
| } |
| |
| // Context menu entries |
| private static final int ID_SELECT_ALL = android.R.id.selectAll; |
| private static final int ID_START_SELECTING_TEXT = android.R.id.startSelectingText; |
| private static final int ID_CUT = android.R.id.cut; |
| private static final int ID_COPY = android.R.id.copy; |
| private static final int ID_PASTE = android.R.id.paste; |
| private static final int ID_COPY_URL = android.R.id.copyUrl; |
| private static final int ID_SWITCH_INPUT_METHOD = android.R.id.switchInputMethod; |
| private static final int ID_ADD_TO_DICTIONARY = android.R.id.addToDictionary; |
| |
| private class MenuHandler implements MenuItem.OnMenuItemClickListener { |
| public boolean onMenuItemClick(MenuItem item) { |
| return onTextContextMenuItem(item.getItemId()); |
| } |
| } |
| |
| /** |
| * Called when a context menu option for the text view is selected. Currently |
| * this will be one of: {@link android.R.id#selectAll}, |
| * {@link android.R.id#startSelectingText}, |
| * {@link android.R.id#cut}, {@link android.R.id#copy}, |
| * {@link android.R.id#paste}, {@link android.R.id#copyUrl}, |
| * or {@link android.R.id#switchInputMethod}. |
| */ |
| public boolean onTextContextMenuItem(int id) { |
| int min = 0; |
| int max = mText.length(); |
| |
| if (isFocused()) { |
| final int selStart = getSelectionStart(); |
| final int selEnd = getSelectionEnd(); |
| |
| min = Math.max(0, Math.min(selStart, selEnd)); |
| max = Math.max(0, Math.max(selStart, selEnd)); |
| } |
| |
| ClipboardManager clip = (ClipboardManager)getContext() |
| .getSystemService(Context.CLIPBOARD_SERVICE); |
| |
| switch (id) { |
| case ID_SELECT_ALL: |
| Selection.setSelection((Spannable) mText, 0, mText.length()); |
| startTextSelectionMode(); |
| return true; |
| |
| case ID_START_SELECTING_TEXT: |
| startTextSelectionMode(); |
| return true; |
| |
| case ID_CUT: |
| clip.setText(mTransformed.subSequence(min, max)); |
| ((Editable) mText).delete(min, max); |
| stopTextSelectionMode(); |
| return true; |
| |
| case ID_COPY: |
| clip.setText(mTransformed.subSequence(min, max)); |
| stopTextSelectionMode(); |
| return true; |
| |
| case ID_PASTE: |
| CharSequence paste = clip.getText(); |
| |
| if (paste != null && paste.length() > 0) { |
| // Paste adds/removes spaces before or after insertion as needed. |
| |
| if (Character.isSpaceChar(paste.charAt(0))) { |
| if (min > 0 && Character.isSpaceChar(mTransformed.charAt(min - 1))) { |
| // Two spaces at beginning of paste: remove one |
| ((Editable) mText).replace(min - 1, min, ""); |
| min = min - 1; |
| max = max - 1; |
| } |
| } else { |
| if (min > 0 && !Character.isSpaceChar(mTransformed.charAt(min - 1))) { |
| // No space at beginning of paste: add one |
| ((Editable) mText).replace(min, min, " "); |
| min = min + 1; |
| max = max + 1; |
| } |
| } |
| |
| if (Character.isSpaceChar(paste.charAt(paste.length() - 1))) { |
| if (max < mText.length() && Character.isSpaceChar(mTransformed.charAt(max))) { |
| // Two spaces at end of paste: remove one |
| ((Editable) mText).replace(max, max + 1, ""); |
| } |
| } else { |
| if (max < mText.length() && !Character.isSpaceChar(mTransformed.charAt(max))) { |
| // No space at end of paste: add one |
| ((Editable) mText).replace(max, max, " "); |
| } |
| } |
| |
| Selection.setSelection((Spannable) mText, max); |
| ((Editable) mText).replace(min, max, paste); |
| stopTextSelectionMode(); |
| } |
| return true; |
| |
| case ID_COPY_URL: |
| URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class); |
| if (urls.length == 1) { |
| clip.setText(urls[0].getURL()); |
| } |
| return true; |
| |
| case ID_SWITCH_INPUT_METHOD: |
| InputMethodManager imm = InputMethodManager.peekInstance(); |
| if (imm != null) { |
| imm.showInputMethodPicker(); |
| } |
| return true; |
| |
| case ID_ADD_TO_DICTIONARY: |
| String word = getWordForDictionary(); |
| |
| if (word != null) { |
| Intent i = new Intent("com.android.settings.USER_DICTIONARY_INSERT"); |
| i.putExtra("word", word); |
| i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); |
| getContext().startActivity(i); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public boolean performLongClick() { |
| if (super.performLongClick()) { |
| mEatTouchRelease = true; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void startTextSelectionMode() { |
| if (!mIsInTextSelectionMode) { |
| if (mSelectionModifierCursorController == null) { |
| Log.w(LOG_TAG, "TextView has no selection controller. Action mode cancelled."); |
| return; |
| } |
| |
| if (!requestFocus()) { |
| return; |
| } |
| |
| selectCurrentWord(); |
| mSelectionModifierCursorController.show(); |
| mIsInTextSelectionMode = true; |
| } |
| } |
| |
| /** |
| * Same as {@link #stopTextSelectionMode()}, except that there is no cursor controller |
| * fade out animation. Needed since the drawable and their alpha values are shared by all |
| * TextViews. Switching from one TextView to another would fade the cursor controllers in the |
| * new one otherwise. |
| */ |
| private void terminateTextSelectionMode() { |
| stopTextSelectionMode(); |
| if (mSelectionModifierCursorController != null) { |
| SelectionModifierCursorController selectionModifierCursorController = |
| (SelectionModifierCursorController) mSelectionModifierCursorController; |
| selectionModifierCursorController.cancelFadeOutAnimation(); |
| } |
| } |
| |
| private void stopTextSelectionMode() { |
| if (mIsInTextSelectionMode) { |
| Selection.setSelection((Spannable) mText, getSelectionEnd()); |
| if (mSelectionModifierCursorController != null) { |
| mSelectionModifierCursorController.hide(); |
| } |
| |
| mIsInTextSelectionMode = false; |
| } |
| } |
| |
| /** |
| * A CursorController instance can be used to control a cursor in the text. |
| * |
| * It can be passed to an {@link ArrowKeyMovementMethod} which can intercepts events |
| * and send them to this object instead of the cursor. |
| */ |
| public interface CursorController { |
| /* Cursor fade-out animation duration, in milliseconds. */ |
| static final int FADE_OUT_DURATION = 400; |
| |
| /** |
| * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. |
| * See also {@link #hide()}. |
| */ |
| public void show(); |
| |
| /** |
| * Hide the cursor controller from screen. |
| * See also {@link #show()}. |
| */ |
| public void hide(); |
| |
| /** |
| * Update the controller's position. |
| */ |
| public void updatePosition(int x, int y); |
| |
| /** |
| * The controller and the cursor's positions can be link by a fixed offset, |
| * computed when the controller is touched, and then maintained as it moves |
| * @return Horizontal offset between the controller and the cursor. |
| */ |
| public float getOffsetX(); |
| |
| /** |
| * @return Vertical offset between the controller and the cursor. |
| */ |
| public float getOffsetY(); |
| |
| /** |
| * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller |
| * a chance to become active and/or visible. |
| * @param event The touch event |
| */ |
| public void onTouchEvent(MotionEvent event); |
| |
| /** |
| * Draws a visual representation of the controller on the canvas. |
| * |
| * Called at the end of {@link #draw(Canvas)}, in the content coordinates system. |
| * @param canvas The Canvas used by this TextView. |
| */ |
| public void draw(Canvas canvas); |
| } |
| |
| private class Handle { |
| Drawable mDrawable; |
| // Vertical extension of the touch region |
| int mTopExtension, mBottomExtension; |
| // Position of the virtual finger position on screen |
| int mHotSpotVerticalPosition; |
| |
| Handle(Drawable drawable) { |
| mDrawable = drawable; |
| } |
| |
| void positionAtCursor(final int offset, boolean bottom) { |
| final int drawableWidth = mDrawable.getIntrinsicWidth(); |
| final int drawableHeight = mDrawable.getIntrinsicHeight(); |
| final int line = mLayout.getLineForOffset(offset); |
| final int lineTop = mLayout.getLineTop(line); |
| final int lineBottom = mLayout.getLineBottom(line); |
| |
| mHotSpotVerticalPosition = lineTop; |
| |
| final Rect bounds = sCursorControllerTempRect; |
| bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - drawableWidth / 2.0) |
| + mScrollX; |
| bounds.top = (bottom ? lineBottom : lineTop) - drawableHeight / 2 + mScrollY; |
| |
| mTopExtension = bottom ? 0 : drawableHeight / 2; |
| mBottomExtension = drawableHeight; |
| |
| // Extend touch region up when editing the last line of text (or a single line) so that |
| // it is easier to grab. |
| if (line == mLayout.getLineCount() - 1) { |
| mTopExtension = (lineBottom - lineTop) - drawableHeight / 2; |
| } |
| |
| bounds.right = bounds.left + drawableWidth; |
| bounds.bottom = bounds.top + drawableHeight; |
| |
| convertFromViewportToContentCoordinates(bounds); |
| mDrawable.setBounds(bounds); |
| postInvalidate(); |
| } |
| |
| boolean hasFingerOn(float x, float y) { |
| // Simulate a 'fat finger' to ease grabbing of the controller. |
| // Expands according to controller image size instead of using dip distance. |
| // Assumes controller imager has a sensible size, proportionnal to screen density. |
| final int drawableWidth = mDrawable.getIntrinsicWidth(); |
| final Rect fingerRect = sCursorControllerTempRect; |
| fingerRect.set((int) (x - drawableWidth / 2.0), |
| (int) (y - mBottomExtension), |
| (int) (x + drawableWidth / 2.0), |
| (int) (y + mTopExtension)); |
| fingerRect.offset(mScrollX, mScrollY); |
| return Rect.intersects(mDrawable.getBounds(), fingerRect); |
| } |
| |
| void postInvalidate() { |
| final Rect bounds = mDrawable.getBounds(); |
| TextView.this.postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom); |
| } |
| |
| void postInvalidateDelayed(long delay) { |
| final Rect bounds = mDrawable.getBounds(); |
| TextView.this.postInvalidateDelayed(delay, bounds.left, bounds.top, |
| bounds.right, bounds.bottom); |
| } |
| } |
| |
| class InsertionPointCursorController implements CursorController { |
| private static final int DELAY_BEFORE_FADE_OUT = 2100; |
| |
| // Whether or not the cursor control is currently visible |
| private boolean mIsVisible = false; |
| // Starting time of the fade timer |
| private long mFadeOutTimerStart; |
| // The cursor controller image |
| private final Handle mHandle; |
| // Used to detect a tap (vs drag) on the controller |
| private long mOnDownTimerStart; |
| // Offset between finger hot point on cursor controller and actual cursor |
| private float mOffsetX, mOffsetY; |
| |
| InsertionPointCursorController() { |
| Resources res = mContext.getResources(); |
| mHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); |
| } |
| |
| public void show() { |
| updateDrawablePosition(); |
| // Has to be done after updateDrawablePosition, so that previous position invalidate |
| // in only done if necessary. |
| mIsVisible = true; |
| } |
| |
| public void hide() { |
| if (mIsVisible) { |
| long time = System.currentTimeMillis(); |
| // Start fading out, only if not already in progress |
| if (time - mFadeOutTimerStart < DELAY_BEFORE_FADE_OUT) { |
| mFadeOutTimerStart = time - DELAY_BEFORE_FADE_OUT; |
| mHandle.postInvalidate(); |
| } |
| } |
| } |
| |
| public void draw(Canvas canvas) { |
| if (mIsVisible) { |
| int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart); |
| if (time <= DELAY_BEFORE_FADE_OUT) { |
| mHandle.postInvalidateDelayed(DELAY_BEFORE_FADE_OUT - time); |
| } else { |
| time -= DELAY_BEFORE_FADE_OUT; |
| if (time <= FADE_OUT_DURATION) { |
| final int alpha = (int) |
| ((255.0 * (FADE_OUT_DURATION - time)) / FADE_OUT_DURATION); |
| mHandle.mDrawable.setAlpha(alpha); |
| mHandle.postInvalidateDelayed(30); |
| } else { |
| mHandle.mDrawable.setAlpha(0); |
| mIsVisible = false; |
| } |
| } |
| mHandle.mDrawable.draw(canvas); |
| } |
| } |
| |
| public void updatePosition(int x, int y) { |
| final int previousOffset = getSelectionStart(); |
| int offset = getHysteresisOffset(x, y, previousOffset); |
| |
| if (offset != previousOffset) { |
| Selection.setSelection((Spannable) mText, offset); |
| updateDrawablePosition(); |
| } |
| } |
| |
| private void updateDrawablePosition() { |
| if (mIsVisible) { |
| // Clear previous cursor controller before bounds are updated |
| mHandle.postInvalidate(); |
| } |
| |
| final int offset = getSelectionStart(); |
| |
| if (offset < 0) { |
| // Should never happen, safety check. |
| Log.w(LOG_TAG, "Update cursor controller position called with no cursor"); |
| mIsVisible = false; |
| return; |
| } |
| |
| mHandle.positionAtCursor(offset, true); |
| |
| mFadeOutTimerStart = System.currentTimeMillis(); |
| mHandle.mDrawable.setAlpha(255); |
| } |
| |
| public void onTouchEvent(MotionEvent event) { |
| if (isFocused() && isTextEditable() && mIsVisible) { |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN : { |
| final float x = event.getX(); |
| final float y = event.getY(); |
| |
| if (mHandle.hasFingerOn(x, y)) { |
| show(); |
| |
| if (mMovement instanceof ArrowKeyMovementMethod) { |
| ((ArrowKeyMovementMethod)mMovement).setCursorController(this); |
| } |
| |
| if (mParent != null) { |
| // Prevent possible scrollView parent from scrolling, so that |
| // we can use auto-scrolling. |
| mParent.requestDisallowInterceptTouchEvent(true); |
| } |
| |
| final Rect bounds = mHandle.mDrawable.getBounds(); |
| mOffsetX = (bounds.left + bounds.right) / 2.0f - x; |
| mOffsetY = mHandle.mHotSpotVerticalPosition - y; |
| |
| mOffsetX += viewportToContentHorizontalOffset(); |
| mOffsetY += viewportToContentVerticalOffset(); |
| |
| mOnDownTimerStart = event.getEventTime(); |
| } |
| break; |
| } |
| |
| case MotionEvent.ACTION_UP : { |
| int time = (int) (event.getEventTime() - mOnDownTimerStart); |
| |
| if (time <= ViewConfiguration.getTapTimeout()) { |
| // A tap on the controller (not a drag) will move the cursor |
| int offset = getOffset((int) event.getX(), (int) event.getY()); |
| Selection.setSelection((Spannable) mText, offset); |
| |
| // Modified by cancelLongPress and prevents the cursor from changing |
| mScrolled = false; |
| } |
| break; |
| } |
| } |
| } |
| } |
| |
| public float getOffsetX() { |
| return mOffsetX; |
| } |
| |
| public float getOffsetY() { |
| return mOffsetY; |
| } |
| } |
| |
| class SelectionModifierCursorController implements CursorController { |
| // Whether or not the selection controls are currently visible |
| private boolean mIsVisible = false; |
| // Whether that start or the end of selection controller is dragged |
| private boolean mStartIsDragged = false; |
| // Starting time of the fade timer |
| private long mFadeOutTimerStart; |
| // Used to detect a tap (vs drag) on the controller |
| private long mOnDownTimerStart; |
| // The cursor controller images |
| private final Handle mStartHandle, mEndHandle; |
| // Offset between finger hot point on active cursor controller and actual cursor |
| private float mOffsetX, mOffsetY; |
| // The offsets of that last touch down event. Remembered to start selection there. |
| private int mMinTouchOffset, mMaxTouchOffset; |
| |
| SelectionModifierCursorController() { |
| Resources res = mContext.getResources(); |
| mStartHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); |
| mEndHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); |
| } |
| |
| public void show() { |
| updateDrawablesPositions(); |
| // Has to be done after updateDrawablePositions, so that previous position invalidate |
| // in only done if necessary. |
| mIsVisible = true; |
| mFadeOutTimerStart = -1; |
| hideInsertionPointCursorController(); |
| } |
| |
| public void hide() { |
| if (mIsVisible && (mFadeOutTimerStart < 0)) { |
| mFadeOutTimerStart = System.currentTimeMillis(); |
| mStartHandle.postInvalidate(); |
| mEndHandle.postInvalidate(); |
| } |
| } |
| |
| public void cancelFadeOutAnimation() { |
| mIsVisible = false; |
| mStartHandle.postInvalidate(); |
| mEndHandle.postInvalidate(); |
| } |
| |
| public void draw(Canvas canvas) { |
| if (mIsVisible) { |
| if (mFadeOutTimerStart >= 0) { |
| int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart); |
| if (time <= FADE_OUT_DURATION) { |
| final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION; |
| mStartHandle.mDrawable.setAlpha(alpha); |
| mEndHandle.mDrawable.setAlpha(alpha); |
| mStartHandle.postInvalidateDelayed(30); |
| mEndHandle.postInvalidateDelayed(30); |
| } else { |
| mStartHandle.mDrawable.setAlpha(0); |
| mEndHandle.mDrawable.setAlpha(0); |
| mIsVisible = false; |
| } |
| } |
| mStartHandle.mDrawable.draw(canvas); |
| mEndHandle.mDrawable.draw(canvas); |
| } |
| } |
| |
| public void updatePosition(int x, int y) { |
| int selectionStart = getSelectionStart(); |
| int selectionEnd = getSelectionEnd(); |
| |
| final int previousOffset = mStartIsDragged ? selectionStart : selectionEnd; |
| int offset = getHysteresisOffset(x, y, previousOffset); |
| |
| // Handle the case where start and end are swapped, making sure start <= end |
| if (mStartIsDragged) { |
| if (offset <= selectionEnd) { |
| if (selectionStart == offset) { |
| return; // no change, no need to redraw; |
| } |
| selectionStart = offset; |
| } else { |
| selectionStart = selectionEnd; |
| selectionEnd = offset; |
| mStartIsDragged = false; |
| } |
| } else { |
| if (offset >= selectionStart) { |
| if (selectionEnd == offset) { |
| return; // no change, no need to redraw; |
| } |
| selectionEnd = offset; |
| } else { |
| selectionEnd = selectionStart; |
| selectionStart = offset; |
| mStartIsDragged = true; |
| } |
| } |
| |
| Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); |
| updateDrawablesPositions(); |
| } |
| |
| private void updateDrawablesPositions() { |
| if (mIsVisible) { |
| // Clear previous cursor controller before bounds are updated |
| mStartHandle.postInvalidate(); |
| mEndHandle.postInvalidate(); |
| } |
| |
| final int selectionStart = getSelectionStart(); |
| final int selectionEnd = getSelectionEnd(); |
| |
| if ((selectionStart < 0) || (selectionEnd < 0)) { |
| // Should never happen, safety check. |
| Log.w(LOG_TAG, "Update selection controller position called with no cursor"); |
| mIsVisible = false; |
| return; |
| } |
| |
| boolean oneLineSelection = mLayout.getLineForOffset(selectionStart) == |
| mLayout.getLineForOffset(selectionEnd); |
| mStartHandle.positionAtCursor(selectionStart, oneLineSelection); |
| mEndHandle.positionAtCursor(selectionEnd, true); |
| |
| mStartHandle.mDrawable.setAlpha(255); |
| mEndHandle.mDrawable.setAlpha(255); |
| } |
| |
| public void onTouchEvent(MotionEvent event) { |
| if (isTextEditable()) { |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: |
| final int x = (int) event.getX(); |
| final int y = (int) event.getY(); |
| |
| // Remember finger down position, to be able to start selection from there |
| mMinTouchOffset = mMaxTouchOffset = mLastTouchOffset = getOffset(x, y); |
| |
| if (mIsVisible) { |
| if (mMovement instanceof ArrowKeyMovementMethod) { |
| boolean isOnStart = mStartHandle.hasFingerOn(x, y); |
| boolean isOnEnd = mEndHandle.hasFingerOn(x, y); |
| if (isOnStart || isOnEnd) { |
| if (mParent != null) { |
| // Prevent possible scrollView parent from scrolling, so |
| // that we can use auto-scrolling. |
| mParent.requestDisallowInterceptTouchEvent(true); |
| } |
| |
| // In case both controllers are under finger (very small |
| // selection region), arbitrarily pick end controller. |
| mStartIsDragged = !isOnEnd; |
| final Handle draggedHandle = |
| mStartIsDragged ? mStartHandle : mEndHandle; |
| final Rect bounds = draggedHandle.mDrawable.getBounds(); |
| mOffsetX = (bounds.left + bounds.right) / 2.0f - x; |
| mOffsetY = draggedHandle.mHotSpotVerticalPosition - y; |
| |
| mOffsetX += viewportToContentHorizontalOffset(); |
| mOffsetY += viewportToContentVerticalOffset(); |
| |
| mOnDownTimerStart = event.getEventTime(); |
| ((ArrowKeyMovementMethod)mMovement).setCursorController(this); |
| } |
| } |
| } |
| break; |
| |
| case MotionEvent.ACTION_UP: |
| int time = (int) (event.getEventTime() - mOnDownTimerStart); |
| |
| if (time <= ViewConfiguration.getTapTimeout()) { |
| // A tap on the controller (not a drag) opens the contextual Copy menu |
| showContextMenu(); |
| } |
| break; |
| |
| case MotionEvent.ACTION_POINTER_DOWN: |
| case MotionEvent.ACTION_POINTER_UP: |
| // Handle multi-point gestures. Keep min and max offset positions. |
| // Only activated for devices that correctly handle multi-touch. |
| if (mContext.getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { |
| updateMinAndMaxOffsets(event); |
| } |
| break; |
| } |
| } |
| } |
| |
| /** |
| * @param event |
| */ |
| private void updateMinAndMaxOffsets(MotionEvent event) { |
| int pointerCount = event.getPointerCount(); |
| for (int index = 0; index < pointerCount; index++) { |
| final int x = (int) event.getX(index); |
| final int y = (int) event.getY(index); |
| int offset = getOffset(x, y); |
| if (offset < mMinTouchOffset) mMinTouchOffset = offset; |
| if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; |
| } |
| } |
| |
| public int getMinTouchOffset() { |
| return mMinTouchOffset; |
| } |
| |
| public int getMaxTouchOffset() { |
| return mMaxTouchOffset; |
| } |
| |
| public float getOffsetX() { |
| return mOffsetX; |
| } |
| |
| public float getOffsetY() { |
| return mOffsetY; |
| } |
| |
| /** |
| * @return true iff this controller is currently used to move the selection start. |
| */ |
| public boolean isSelectionStartDragged() { |
| return mIsVisible && mStartIsDragged; |
| } |
| } |
| |
| private void hideInsertionPointCursorController() { |
| if (mInsertionPointCursorController != null) { |
| mInsertionPointCursorController.hide(); |
| } |
| } |
| |
| private void hideControllers() { |
| hideInsertionPointCursorController(); |
| stopTextSelectionMode(); |
| } |
| |
| private int getOffsetForHorizontal(int line, int x) { |
| x -= getTotalPaddingLeft(); |
| // Clamp the position to inside of the view. |
| x = Math.max(0, x); |
| x = Math.min(getWidth() - getTotalPaddingRight() - 1, x); |
| x += getScrollX(); |
| return getLayout().getOffsetForHorizontal(line, x); |
| } |
| |
| /** |
| * Get the offset character closest to the specified absolute position. |
| * |
| * @param x The horizontal absolute position of a point on screen |
| * @param y The vertical absolute position of a point on screen |
| * @return the character offset for the character whose position is closest to the specified |
| * position. Returns -1 if there is no layout. |
| * |
| * @hide |
| */ |
| public int getOffset(int x, int y) { |
| if (getLayout() == null) return -1; |
| |
| y -= getTotalPaddingTop(); |
| // Clamp the position to inside of the view. |
| y = Math.max(0, y); |
| y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y); |
| y += getScrollY(); |
| |
| final int line = getLayout().getLineForVertical(y); |
| final int offset = getOffsetForHorizontal(line, x); |
| return offset; |
| } |
| |
| int getHysteresisOffset(int x, int y, int previousOffset) { |
| final Layout layout = getLayout(); |
| if (layout == null) return -1; |
| |
| y -= getTotalPaddingTop(); |
| // Clamp the position to inside of the view. |
| y = Math.max(0, y); |
| y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y); |
| y += getScrollY(); |
| |
| int line = getLayout().getLineForVertical(y); |
| |
| final int previousLine = layout.getLineForOffset(previousOffset); |
| final int previousLineTop = layout.getLineTop(previousLine); |
| final int previousLineBottom = layout.getLineBottom(previousLine); |
| final int hysteresisThreshold = (previousLineBottom - previousLineTop) / 2; |
| |
| // If new line is just before or after previous line and y position is less than |
| // hysteresisThreshold away from previous line, keep cursor on previous line. |
| if (((line == previousLine + 1) && ((y - previousLineBottom) < hysteresisThreshold)) || |
| ((line == previousLine - 1) && ((previousLineTop - y) < hysteresisThreshold))) { |
| line = previousLine; |
| } |
| |
| return getOffsetForHorizontal(line, x); |
| } |
| |
| @ViewDebug.ExportedProperty |
| private CharSequence mText; |
| private CharSequence mTransformed; |
| private BufferType mBufferType = BufferType.NORMAL; |
| |
| private int mInputType = EditorInfo.TYPE_NULL; |
| private CharSequence mHint; |
| private Layout mHintLayout; |
| |
| private KeyListener mInput; |
| |
| private MovementMethod mMovement; |
| private TransformationMethod mTransformation; |
| private ChangeWatcher mChangeWatcher; |
| |
| private ArrayList<TextWatcher> mListeners = null; |
| |
| // display attributes |
| private final TextPaint mTextPaint; |
| private boolean mUserSetTextScaleX; |
| private final Paint mHighlightPaint; |
| private int mHighlightColor = 0xCC475925; |
| private Layout mLayout; |
| |
| private long mShowCursor; |
| private Blink mBlink; |
| private boolean mCursorVisible = true; |
| |
| // Cursor Controllers. Null when disabled. |
| private CursorController mInsertionPointCursorController; |
| private CursorController mSelectionModifierCursorController; |
| private boolean mIsInTextSelectionMode = false; |
| private int mLastTouchOffset = -1; |
| // Created once and shared by different CursorController helper methods. |
| // Only one cursor controller is active at any time which prevent race conditions. |
| private static Rect sCursorControllerTempRect = new Rect(); |
| |
| private boolean mSelectAllOnFocus = false; |
| |
| private int mGravity = Gravity.TOP | Gravity.LEFT; |
| private boolean mHorizontallyScrolling; |
| |
| private int mAutoLinkMask; |
| private boolean mLinksClickable = true; |
| |
| private float mSpacingMult = 1; |
| private float mSpacingAdd = 0; |
| |
| private static final int LINES = 1; |
| private static final int EMS = LINES; |
| private static final int PIXELS = 2; |
| |
| private int mMaximum = Integer.MAX_VALUE; |
| private int mMaxMode = LINES; |
| private int mMinimum = 0; |
| private int mMinMode = LINES; |
| |
| private int mMaxWidth = Integer.MAX_VALUE; |
| private int mMaxWidthMode = PIXELS; |
| private int mMinWidth = 0; |
| private int mMinWidthMode = PIXELS; |
| |
| private boolean mSingleLine; |
| private int mDesiredHeightAtMeasure = -1; |
| private boolean mIncludePad = true; |
| |
| // tmp primitives, so we don't alloc them on each draw |
| private Path mHighlightPath; |
| private boolean mHighlightPathBogus = true; |
| private static final RectF sTempRect = new RectF(); |
| |
| // XXX should be much larger |
| private static final int VERY_WIDE = 16384; |
| |
| private static final int BLINK = 500; |
| |
| private static final int ANIMATED_SCROLL_GAP = 250; |
| private long mLastScroll; |
| private Scroller mScroller = null; |
| |
| private BoringLayout.Metrics mBoring; |
| private BoringLayout.Metrics mHintBoring; |
| |
| private BoringLayout mSavedLayout, mSavedHintLayout; |
| |
| private static final InputFilter[] NO_FILTERS = new InputFilter[0]; |
| private InputFilter[] mFilters = NO_FILTERS; |
| private static final Spanned EMPTY_SPANNED = new SpannedString(""); |
| } |