alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Android example source code file (TextView.java)

This example Android source code file (TextView.java) is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Android by Example" TM.

Java - Android tags/keywords

android, charsequence, colorstatelist, drawables, editable, input, inputcontenttype, inputmethodmanager, lines, os, override, savedstate, spannable, spanned, string, text, textview, ui, unknown_boring, view

The TextView.java Android example source code

/*
 * 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.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.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.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.ViewDebug;
import android.view.ViewRoot;
import android.view.ViewTreeObserver;
import android.view.ViewGroup.LayoutParams;
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
 * <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 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);
    }

    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("TextView", "Failure reading input extras", e);
                } catch (IOException e) {
                    Log.w("TextView", "Failure reading input extras", e);
                }
                break;
            }
        }
        a.recycle();

        BufferType bufferType = BufferType.EDITABLE;

        if ((inputType&(EditorInfo.TYPE_MASK_CLASS
                |EditorInfo.TYPE_MASK_VARIATION))
                == (EditorInfo.TYPE_CLASS_TEXT
                        |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
            password = true;
        }

        if (inputMethod != null) {
            Class c;

            try {
                c = Class.forName(inputMethod.toString());
            } catch (ClassNotFoundException ex) {
                throw new RuntimeException(ex);
            }

            try {
                mInput = (KeyListener) c.newInstance();
            } catch (InstantiationException ex) {
                throw new RuntimeException(ex);
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(ex);
            }
            try {
                mInputType = inputType != EditorInfo.TYPE_NULL
                        ? inputType
                        : mInput.getInputType();
            } catch (IncompatibleClassChangeError e) {
                mInputType = EditorInfo.TYPE_CLASS_TEXT;
            }
        } else if (digits != null) {
            mInput = DigitsKeyListener.getInstance(digits.toString());
            // 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;
        } 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);
    }

    private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
        Typeface tf = null;
        switch (typefaceIndex) {
            case SANS:
                tf = Typeface.SANS_SERIF;
                break;

            case SERIF:
                tf = Typeface.SERIF;
                break;

            case MONOSPACE:
                tf = Typeface.MONOSPACE;
                break;
        }

        setTypeface(tf, styleIndex);
    }

    /**
     * Sets the typeface and style in which the text should be displayed,
     * and turns on the fake bold and italic bits in the Paint if the
     * Typeface that you provided does not have all the bits in the
     * style that you specified.
     *
     * @attr ref android.R.styleable#TextView_typeface
     * @attr ref android.R.styleable#TextView_textStyle
     */
    public void setTypeface(Typeface tf, int style) {
        if (style > 0) {
            if (tf == null) {
                tf = Typeface.defaultFromStyle(style);
            } else {
                tf = Typeface.create(tf, style);
            }

            setTypeface(tf);
            // now compute what (if any) algorithmic styling is needed
            int typefaceStyle = tf != null ? tf.getStyle() : 0;
            int need = style & ~typefaceStyle;
            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
        } else {
            mTextPaint.setFakeBoldText(false);
            mTextPaint.setTextSkewX(0);
            setTypeface(tf);
        }
    }

    /**
     * Subclasses override this to specify that they have a KeyListener
     * by default even if not specifically called for in the XML options.
     */
    protected boolean getDefaultEditable() {
        return false;
    }

    /**
     * Subclasses override this to specify a default movement method.
     */
    protected MovementMethod getDefaultMovementMethod() {
        return null;
    }

    /**
     * Return the text the TextView is displaying. If setText() was called with
     * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
     * the return value from this method to Spannable or Editable, respectively.
     *
     * Note: The content of the return value should not be modified. If you want
     * a modifiable one, you should make your own copy first.
     */
    @ViewDebug.CapturedViewProperty
    public CharSequence getText() {
        return mText;
    }

    /**
     * Returns the length, in characters, of the text managed by this TextView
     */
    public int length() {
        return mText.length();
    }

    /**
     * Return the text the TextView is displaying as an Editable object.  If
     * the text is not editable, null is returned.
     *
     * @see #getText
     */
    public Editable getEditableText() {
        return (mText instanceof Editable) ? (Editable)mText : null;
    }

    /**
     * @return the height of one standard line in pixels.  Note that markup
     * within the text can cause individual lines to be taller or shorter
     * than this height, and the layout may contain additional first-
     * or last-line padding.
     */
    public int getLineHeight() {
        return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult
                          + mSpacingAdd);
    }

    /**
     * @return the Layout that is currently being used to display the text.
     * This can be null if the text or width has recently changes.
     */
    public final Layout getLayout() {
        return mLayout;
    }

    /**
     * @return the current key listener for this TextView.
     * This will frequently be null for non-EditText TextViews.
     */
    public final KeyListener getKeyListener() {
        return mInput;
    }

    /**
     * Sets the key listener to be used with this TextView.  This can be null
     * to disallow user input.  Note that this method has significant and
     * subtle interactions with soft keyboards and other input method:
     * see {@link KeyListener#getInputType() KeyListener.getContentType()}
     * for important details.  Calling this method will replace the current
     * content type of the text view with the content type returned by the
     * key listener.
     * <p>
     * Be warned that if you want a TextView with a key listener or movement
     * method not to be focusable, or if you want a TextView without a
     * key listener or movement method to be focusable, you must call
     * {@link #setFocusable} again after calling this to get the focusability
     * back the way you want it.
     *
     * @attr ref android.R.styleable#TextView_numeric
     * @attr ref android.R.styleable#TextView_digits
     * @attr ref android.R.styleable#TextView_phoneNumber
     * @attr ref android.R.styleable#TextView_inputMethod
     * @attr ref android.R.styleable#TextView_capitalize
     * @attr ref android.R.styleable#TextView_autoText
     */
    public void setKeyListener(KeyListener input) {
        setKeyListenerOnly(input);
        fixFocusableAndClickableSettings();

        if (input != null) {
            try {
                mInputType = mInput.getInputType();
            } catch (IncompatibleClassChangeError e) {
                mInputType = EditorInfo.TYPE_CLASS_TEXT;
            }
            if ((mInputType&EditorInfo.TYPE_MASK_CLASS)
                    == EditorInfo.TYPE_CLASS_TEXT) {
                if (mSingleLine) {
                    mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
                } else {
                    mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
                }
            }
        } else {
            mInputType = EditorInfo.TYPE_NULL;
        }

        InputMethodManager imm = InputMethodManager.peekInstance();
        if (imm != null) imm.restartInput(this);
    }

    private void setKeyListenerOnly(KeyListener input) {
        mInput = input;
        if (mInput != null && !(mText instanceof Editable))
            setText(mText);

        setFilters((Editable) mText, mFilters);
    }

    /**
     * @return the movement method being used for this TextView.
     * This will frequently be null for non-EditText TextViews.
     */
    public final MovementMethod getMovementMethod() {
        return mMovement;
    }

    /**
     * Sets the movement method (arrow key handler) to be used for
     * this TextView.  This can be null to disallow using the arrow keys
     * to move the cursor or scroll the view.
     * <p>
     * Be warned that if you want a TextView with a key listener or movement
     * method not to be focusable, or if you want a TextView without a
     * key listener or movement method to be focusable, you must call
     * {@link #setFocusable} again after calling this to get the focusability
     * back the way you want it.
     */
    public final void setMovementMethod(MovementMethod movement) {
        mMovement = movement;

        if (mMovement != null && !(mText instanceof Spannable))
            setText(mText);

        fixFocusableAndClickableSettings();
    }

    private void fixFocusableAndClickableSettings() {
        if ((mMovement != null) || mInput != null) {
            setFocusable(true);
            setClickable(true);
            setLongClickable(true);
        } else {
            setFocusable(false);
            setClickable(false);
            setLongClickable(false);
        }
    }

    /**
     * @return the current transformation method for this TextView.
     * This will frequently be null except for single-line and password
     * fields.
     */
    public final TransformationMethod getTransformationMethod() {
        return mTransformation;
    }

    /**
     * Sets the transformation that is applied to the text that this
     * TextView is displaying.
     *
     * @attr ref android.R.styleable#TextView_password
     * @attr ref android.R.styleable#TextView_singleLine
     */
    public final void setTransformationMethod(TransformationMethod method) {
        if (method == mTransformation) {
            // Avoid the setText() below if the transformation is
            // the same.
            return;
        }
        if (mTransformation != null) {
            if (mText instanceof Spannable) {
                ((Spannable) mText).removeSpan(mTransformation);
            }
        }

        mTransformation = method;

        setText(mText);
    }

    /**
     * Returns the top padding of the view, plus space for the top
     * Drawable if any.
     */
    public int getCompoundPaddingTop() {
        final Drawables dr = mDrawables;
        if (dr == null || dr.mDrawableTop == null) {
            return mPaddingTop;
        } else {
            return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
        }
    }

    /**
     * Returns the bottom padding of the view, plus space for the bottom
     * Drawable if any.
     */
    public int getCompoundPaddingBottom() {
        final Drawables dr = mDrawables;
        if (dr == null || dr.mDrawableBottom == null) {
            return mPaddingBottom;
        } else {
            return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
        }
    }

    /**
     * Returns the left padding of the view, plus space for the left
     * Drawable if any.
     */
    public int getCompoundPaddingLeft() {
        final Drawables dr = mDrawables;
        if (dr == null || dr.mDrawableLeft == null) {
            return mPaddingLeft;
        } else {
            return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
        }
    }

    /**
     * Returns the right padding of the view, plus space for the right
     * Drawable if any.
     */
    public int getCompoundPaddingRight() {
        final Drawables dr = mDrawables;
        if (dr == null || dr.mDrawableRight == null) {
            return mPaddingRight;
        } else {
            return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
        }
    }

    /**
     * Returns the extended top padding of the view, including both the
     * top Drawable if any and any extra space to keep more than maxLines
     * of text from showing.  It is only valid to call this after measuring.
     */
    public int getExtendedPaddingTop() {
        if (mMaxMode != LINES) {
            return getCompoundPaddingTop();
        }

        if (mLayout.getLineCount() <= mMaximum) {
            return getCompoundPaddingTop();
        }

        int top = getCompoundPaddingTop();
        int bottom = getCompoundPaddingBottom();
        int viewht = getHeight() - top - bottom;
        int layoutht = mLayout.getLineTop(mMaximum);

        if (layoutht >= viewht) {
            return top;
        }

        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        if (gravity == Gravity.TOP) {
            return top;
        } else if (gravity == Gravity.BOTTOM) {
            return top + viewht - layoutht;
        } else { // (gravity == Gravity.CENTER_VERTICAL)
            return top + (viewht - layoutht) / 2;
        }
    }

    /**
     * Returns the extended bottom padding of the view, including both the
     * bottom Drawable if any and any extra space to keep more than maxLines
     * of text from showing.  It is only valid to call this after measuring.
     */
    public int getExtendedPaddingBottom() {
        if (mMaxMode != LINES) {
            return getCompoundPaddingBottom();
        }

        if (mLayout.getLineCount() <= mMaximum) {
            return getCompoundPaddingBottom();
        }

        int top = getCompoundPaddingTop();
        int bottom = getCompoundPaddingBottom();
        int viewht = getHeight() - top - bottom;
        int layoutht = mLayout.getLineTop(mMaximum);

        if (layoutht >= viewht) {
            return bottom;
        }

        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        if (gravity == Gravity.TOP) {
            return bottom + viewht - layoutht;
        } else if (gravity == Gravity.BOTTOM) {
            return bottom;
        } else { // (gravity == Gravity.CENTER_VERTICAL)
            return bottom + (viewht - layoutht) / 2;
        }
    }

    /**
     * Returns the total left padding of the view, including the left
     * Drawable if any.
     */
    public int getTotalPaddingLeft() {
        return getCompoundPaddingLeft();
    }

    /**
     * Returns the total right padding of the view, including the right
     * Drawable if any.
     */
    public int getTotalPaddingRight() {
        return getCompoundPaddingRight();
    }

    /**
     * Returns the total top padding of the view, including the top
     * Drawable if any, the extra space to keep more than maxLines
     * from showing, and the vertical offset for gravity, if any.
     */
    public int getTotalPaddingTop() {
        return getExtendedPaddingTop() + getVerticalOffset(true);
    }

    /**
     * Returns the total bottom padding of the view, including the bottom
     * Drawable if any, the extra space to keep more than maxLines
     * from showing, and the vertical offset for gravity, if any.
     */
    public int getTotalPaddingBottom() {
        return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
    }

    /**
     * Sets the Drawables (if any) to appear to the left of, above,
     * to the right of, and below the text.  Use null if you do not
     * want a Drawable there.  The Drawables must already have had
     * {@link Drawable#setBounds} called.
     *
     * @attr ref android.R.styleable#TextView_drawableLeft
     * @attr ref android.R.styleable#TextView_drawableTop
     * @attr ref android.R.styleable#TextView_drawableRight
     * @attr ref android.R.styleable#TextView_drawableBottom
     */
    public void setCompoundDrawables(Drawable left, Drawable top,
                                     Drawable right, Drawable bottom) {
        Drawables dr = mDrawables;

        final boolean drawables = left != null || top != null
                || right != null || bottom != null;

        if (!drawables) {
            // Clearing drawables...  can we free the data structure?
            if (dr != null) {
                if (dr.mDrawablePadding == 0) {
                    mDrawables = null;
                } else {
                    // We need to retain the last set padding, so just clear
                    // out all of the fields in the existing structure.
                    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.

* * @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.

* * @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.

* * @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.

* * @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 and have add added to it. * * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier */ public void setLineSpacing(float add, float mult) { mSpacingMult = mult; mSpacingAdd = add; if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } /** * Convenience method: Append the specified text to the TextView's * display buffer, upgrading it to BufferType.EDITABLE if it was * not already editable. */ public final void append(CharSequence text) { append(text, 0, text.length()); } /** * Convenience method: Append the specified text slice to the TextView's * display buffer, upgrading it to BufferType.EDITABLE if it was * not already editable. */ public void append(CharSequence text, int start, int end) { if (!(mText instanceof Editable)) { setText(mText, BufferType.EDITABLE); } ((Editable) mText).append(text, start, end); } private void updateTextColors() { boolean inval = false; int color = mTextColor.getColorForState(getDrawableState(), 0); if (color != mCurTextColor) { mCurTextColor = color; inval = true; } if (mLinkTextColor != null) { color = mLinkTextColor.getColorForState(getDrawableState(), 0); if (color != mTextPaint.linkColor) { mTextPaint.linkColor = color; inval = true; } } if (mHintTextColor != null) { color = mHintTextColor.getColorForState(getDrawableState(), 0); if (color != mCurHintTextColor && mText.length() == 0) { mCurHintTextColor = color; inval = true; } } if (inval) { invalidate(); } } @Override protected void drawableStateChanged() { super.drawableStateChanged(); if (mTextColor != null && mTextColor.isStateful() || (mHintTextColor != null && mHintTextColor.isStateful()) || (mLinkTextColor != null && mLinkTextColor.isStateful())) { updateTextColors(); } final Drawables dr = mDrawables; if (dr != null) { int[] state = getDrawableState(); if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) { dr.mDrawableTop.setState(state); } if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) { dr.mDrawableBottom.setState(state); } if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) { dr.mDrawableLeft.setState(state); } if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) { dr.mDrawableRight.setState(state); } } } /** * 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 + "}"; } 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 = Selection.getSelectionStart(mText); end = Selection.getSelectionEnd(mText); if (start >= 0 || end >= 0) { // Or save state if there is a selection save = true; } } if (save) { SavedState ss = new SavedState(superState); // XXX Should also save the current scroll position! ss.selStart = start; ss.selEnd = end; if (mText instanceof Spanned) { /* * Calling setText() strips off any ChangeWatchers; * strip them now to avoid leaking references. * But do it to a copy so that if there are any * further changes to the text of this view, it * won't get into an inconsistent state. */ Spannable sp = new SpannableString(mText); for (ChangeWatcher cw : sp.getSpans(0, sp.length(), ChangeWatcher.class)) { sp.removeSpan(cw); } ss.text = sp; } else { ss.text = mText.toString(); } if (isFocused() && start >= 0 && end >= 0) { ss.frozenWithFocus = true; } 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("TextView", "Saved cursor position " + ss.selStart + "/" + ss.selEnd + " out of range for " + restored + "text " + mText); } else { Selection.setSelection((Spannable) mText, ss.selStart, ss.selEnd); if (ss.frozenWithFocus) { mFrozenWithFocus = true; } } } } 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 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); } } /** * Sets the TextView to display the specified slice of the specified * char array. You must promise that you will not change the contents * of the array except for right before another call to setText(), * since the TextView has no way to know that the text * has changed and that it needs to invalidate and re-layout. */ public final void setText(char[] text, int start, int len) { int oldlen = 0; if (start < 0 || len < 0 || start + len > text.length) { throw new IndexOutOfBoundsException(start + ", " + len); } /* * We must do the before-notification here ourselves because if * the old text is a CharWrapper we destroy it before calling * into the normal path. */ if (mText != null) { oldlen = mText.length(); sendBeforeTextChanged(mText, 0, oldlen, len); } else { sendBeforeTextChanged("", 0, 0, len); } if (mCharWrapper == null) { mCharWrapper = new CharWrapper(text, start, len); } else { mCharWrapper.set(text, start, len); } setText(mCharWrapper, mBufferType, false, oldlen); } private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations { private char[] mChars; private int mStart, mLength; public CharWrapper(char[] chars, int start, int len) { mChars = chars; mStart = start; mLength = len; } /* package */ void set(char[] chars, int start, int len) { mChars = chars; mStart = start; mLength = len; } public int length() { return mLength; } public char charAt(int off) { return mChars[off + mStart]; } public String toString() { return new String(mChars, mStart, mLength); } public CharSequence subSequence(int start, int end) { if (start < 0 || end < 0 || start > mLength || end > mLength) { throw new IndexOutOfBoundsException(start + ", " + end); } return new String(mChars, start + mStart, end - start); } public void getChars(int start, int end, char[] buf, int off) { if (start < 0 || end < 0 || start > mLength || end > mLength) { throw new IndexOutOfBoundsException(start + ", " + end); } System.arraycopy(mChars, start + mStart, buf, off, end - start); } public void drawText(Canvas c, int start, int end, float x, float y, Paint p) { c.drawText(mChars, start + mStart, end - start, x, y, p); } public float measureText(int start, int end, Paint p) { return p.measureText(mChars, start + mStart, end - start); } public int getTextWidths(int start, int end, float[] widths, Paint p) { return p.getTextWidths(mChars, start + mStart, end - start, widths); } } /** * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)}, * except that the cursor position (if any) is retained in the new text. * * @see #setText(CharSequence, android.widget.TextView.BufferType) */ public final void setTextKeepState(CharSequence text, BufferType type) { int start = getSelectionStart(); int end = getSelectionEnd(); int len = text.length(); setText(text, type); if (start >= 0 || end >= 0) { if (mText instanceof Spannable) { Selection.setSelection((Spannable) mText, Math.max(0, Math.min(start, len)), Math.max(0, Math.min(end, len))); } } } @android.view.RemotableViewMethod public final void setText(int resid) { setText(getContext().getResources().getText(resid)); } public final void setText(int resid, BufferType type) { setText(getContext().getResources().getText(resid), type); } /** * Sets the text to be displayed when the text of the TextView is empty. * Null means to use the normal empty text. The hint does not currently * participate in determining the size of the view. * * @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(); } mInputType = type; if (direct) mInput = input; else { setKeyListenerOnly(input); } } /** * Get the type of the content. * * @see #setInputType(int) * @see android.text.InputType */ public int getInputType() { return mInputType; } /** * Change the editor type integer associated with the text view, which * will be reported to an IME with {@link EditorInfo#imeOptions} when it * has focus. * @see #getImeOptions * @see android.view.inputmethod.EditorInfo * @attr ref android.R.styleable#TextView_imeOptions */ public void setImeOptions(int imeOptions) { if (mInputContentType == null) { mInputContentType = new InputContentType(); } mInputContentType.imeOptions = imeOptions; } /** * Get the type of the IME editor. * * @see #setImeOptions(int) * @see android.view.inputmethod.EditorInfo */ public int getImeOptions() { return mInputContentType != null ? mInputContentType.imeOptions : EditorInfo.IME_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)View * @see EditorInfo#extras * @attr ref android.R.styleable#TextView_editorExtras */ public Bundle getInputExtras(boolean create) { if (mInputContentType == null) { if (!create) return null; mInputContentType = new InputContentType(); } if (mInputContentType.extras == null) { if (!create) return null; mInputContentType.extras = new Bundle(); } return mInputContentType.extras; } /** * Returns the error message that was set to be displayed with * {@link #setError}, or <code>null 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 is null, the error message and icon * will be cleared. */ @android.view.RemotableViewMethod public void setError(CharSequence error) { if (error == null) { setError(null, null); } else { Drawable dr = getContext().getResources(). getDrawable(com.android.internal.R.drawable. indicator_input_error); dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight()); setError(error, dr); } } /** * Sets the right-hand compound drawable of the TextView to the specified * icon and sets an error message that will be displayed in a popup when * the TextView has focus. The icon and error message will be reset to * null when any key events cause changes to the TextView's text. The * drawable must already have had {@link Drawable#setBounds} set on it. * If the <code>error is null, the error message will * be cleared (and you should provide a <code>null icon as well). */ public void setError(CharSequence error, Drawable icon) { error = TextUtils.stringOrSpannedString(error); mError = error; mErrorWasChanged = true; final Drawables dr = mDrawables; if (dr != null) { 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 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 = Selection.getSelectionEnd(mText); invalidateCursor(where, where, where); } private void invalidateCursor(int a, int b, int c) { if (mLayout == null) { invalidate(); } else { if (a >= 0 || b >= 0 || c >= 0) { int first = Math.min(Math.min(a, b), c); int last = Math.max(Math.max(a, b), c); int line = mLayout.getLineForOffset(first); int top = mLayout.getLineTop(line); // This is ridiculous, but the descent from the line above // can hang down into the line we really want to redraw, // so we have to invalidate part of the line above to make // sure everything that needs to be redrawn really is. // (But not the whole line above, because that would cause // the same problem with the descenders on the line above it!) if (line > 0) { top -= mLayout.getLineDescent(line - 1); } int line2; if (first == last) line2 = line; else line2 = mLayout.getLineForOffset(last); int bottom = mLayout.getLineTop(line2 + 1); int voffset = getVerticalOffset(true); int left = getCompoundPaddingLeft() + mScrollX; invalidate(left, top + voffset + getExtendedPaddingTop(), left + getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(), bottom + voffset + getExtendedPaddingTop()); } } } private void registerForPreDraw() { final ViewTreeObserver observer = getViewTreeObserver(); if (observer == null) { return; } if (mPreDrawState == PREDRAW_NOT_REGISTERED) { observer.addOnPreDrawListener(this); mPreDrawState = PREDRAW_PENDING; } else if (mPreDrawState == PREDRAW_DONE) { mPreDrawState = PREDRAW_PENDING; } // else state is PREDRAW_PENDING, so keep waiting. } /** * {@inheritDoc} */ public boolean onPreDraw() { if (mPreDrawState != PREDRAW_PENDING) { return true; } if (mLayout == null) { assumeLayout(); } boolean changed = false; if (mMovement != null) { int curs = Selection.getSelectionEnd(mText); /* * TODO: This should really only keep the end in view if * it already was before the text changed. I'm not sure * of a good way to tell from here if it was. */ if (curs < 0 && (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { curs = mText.length(); } if (curs >= 0) { changed = bringPointIntoView(curs); } } else { changed = bringTextIntoView(); } mPreDrawState = PREDRAW_DONE; return !changed; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mTemporaryDetach = false; if (mShowErrorAfterAttach) { showError(); mShowErrorAfterAttach = false; } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mPreDrawState != PREDRAW_NOT_REGISTERED) { final ViewTreeObserver observer = getViewTreeObserver(); if (observer != null) { observer.removeOnPreDrawListener(this); mPreDrawState = PREDRAW_NOT_REGISTERED; } } if (mError != null) { hideError(); } } @Override protected boolean isPaddingOffsetRequired() { return mShadowRadius != 0 || 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 = Selection.getSelectionStart(mText); selEnd = Selection.getSelectionEnd(mText); if (mCursorVisible && selStart >= 0 && isEnabled()) { if (mHighlightPath == null) mHighlightPath = new Path(); if (selStart == selEnd) { if ((SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) { if (mHighlightPathBogus) { mHighlightPath.reset(); mLayout.getCursorPath(selStart, mHighlightPath, mText); mHighlightPathBogus = false; } // XXX should pass to skin instead of drawing directly mHighlightPaint.setColor(cursorcolor); mHighlightPaint.setStyle(Paint.Style.STROKE); highlight = mHighlightPath; } } else { if (mHighlightPathBogus) { mHighlightPath.reset(); mLayout.getSelectionPath(selStart, selEnd, mHighlightPath); mHighlightPathBogus = false; } // XXX should pass to skin instead of drawing directly mHighlightPaint.setColor(mHighlightColor); mHighlightPaint.setStyle(Paint.Style.FILL); highlight = mHighlightPath; } } } /* Comment out until we decide what to do about animations boolean isLinearTextOn = false; if (currentTransformation != null) { isLinearTextOn = mTextPaint.isLinearTextOn(); Matrix m = currentTransformation.getMatrix(); if (!m.isIdentity()) { // mTextPaint.setLinearTextOn(true); } } */ final InputMethodState ims = mInputMethodState; if (ims != null && ims.mBatchEditNesting == 0) { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { if (imm.isActive(this)) { boolean reported = false; if (ims.mContentChanged || 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(); } @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; } } if (mInput != null) { /* * Keep track of what the error was before doing the input * so that if an input filter changed the error, we leave * that error showing. Otherwise, we take down whatever * error was showing when the user types something. */ mErrorWasChanged = false; boolean doDown = true; if (otherEvent != null) { try { beginBatchEdit(); boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent); if (mError != null && !mErrorWasChanged) { setError(null, null); } doDown = false; if (handled) { return -1; } } catch (AbstractMethodError e) { // onKeyOther was added after 1.0, so if it isn't // implemented we need to try to dispatch as a regular down. } finally { endBatchEdit(); } } if (doDown) { beginBatchEdit(); if (mInput.onKeyDown(this, (Editable) mText, keyCode, event)) { endBatchEdit(); if (mError != null && !mErrorWasChanged) { setError(null, null); } return 1; } endBatchEdit(); } } // bug 650865: sometimes we get a key event before a layout. // don't try to move around if we don't know the layout. if (mMovement != null && mLayout != null) { boolean doDown = true; if (otherEvent != null) { try { boolean handled = mMovement.onKeyOther(this, (Spannable) mText, otherEvent); doDown = false; if (handled) { return -1; } } catch (AbstractMethodError e) { // onKeyOther was added after 1.0, so if it isn't // implemented we need to try to dispatch as a regular down. } } if (doDown) { if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event)) return 2; } } return 0; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (!isEnabled()) { return super.onKeyUp(keyCode, event); } switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: /* * If there is a click listener, just call through to * super, which will invoke it. * * If there isn't a click listener, try to show the soft * input method. (It will also * call performClick(), but that won't do anything in * this case.) */ if (mOnClickListener == null) { if (mMovement != null && mText instanceof Editable && mLayout != null && onCheckIsTextEditor()) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(this, 0); } } return super.onKeyUp(keyCode, event); case KeyEvent.KEYCODE_ENTER: if (mInputContentType != null && mInputContentType.onEditorActionListener != null && mInputContentType.enterDown) { mInputContentType.enterDown = false; if (mInputContentType.onEditorActionListener.onEditorAction( this, EditorInfo.IME_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); } } 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 = Selection.getSelectionStart(mText); outAttrs.initialSelEnd = Selection.getSelectionEnd(mText); outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType); return ic; } } return null; } /** * If this TextView contains editable content, extract a portion of it * based on the information in <var>request in to outText. * @return Returns true if the text was successfully extracted, else false. */ public boolean extractText(ExtractedTextRequest request, ExtractedText outText) { return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, outText); } static final int EXTRACT_NOTHING = -2; static final int EXTRACT_UNKNOWN = -1; boolean extractTextInternal(ExtractedTextRequest request, int partialStartOffset, int partialEndOffset, int delta, ExtractedText outText) { final CharSequence content = mText; if (content != null) { if (partialStartOffset != EXTRACT_NOTHING) { final int N = content.length(); if (partialStartOffset < 0) { outText.partialStartOffset = outText.partialEndOffset = -1; partialStartOffset = 0; partialEndOffset = N; } else { // Now use the delta to determine the actual amount of text // we need. partialEndOffset += delta; // Adjust offsets to ensure we contain full spans. if (content instanceof Spanned) { Spanned spanned = (Spanned)content; Object[] spans = spanned.getSpans(partialStartOffset, partialEndOffset, ParcelableSpan.class); int i = spans.length; while (i > 0) { i--; int j = spanned.getSpanStart(spans[i]); if (j < partialStartOffset) partialStartOffset = j; j = spanned.getSpanEnd(spans[i]); if (j > partialEndOffset) partialEndOffset = j; } } outText.partialStartOffset = partialStartOffset; outText.partialEndOffset = partialEndOffset - delta; if (partialStartOffset > N) { partialStartOffset = N; } else if (partialStartOffset < 0) { partialStartOffset = 0; } if (partialEndOffset > N) { partialEndOffset = N; } else if (partialEndOffset < 0) { partialEndOffset = 0; } } if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { outText.text = content.subSequence(partialStartOffset, partialEndOffset); } else { outText.text = TextUtils.substring(content, partialStartOffset, partialEndOffset); } } else { outText.partialStartOffset = 0; outText.partialEndOffset = 0; outText.text = ""; } outText.flags = 0; if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) { outText.flags |= ExtractedText.FLAG_SELECTING; } if (mSingleLine) { outText.flags |= ExtractedText.FLAG_SINGLE_LINE; } outText.startOffset = 0; outText.selectionStart = Selection.getSelectionStart(content); outText.selectionEnd = Selection.getSelectionEnd(content); 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(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(TAG, "Reporting extracted start=" + ims.mTmpExtracted.partialStartOffset + " end=" + ims.mTmpExtracted.partialEndOffset + ": " + ims.mTmpExtracted.text); imm.updateExtractedText(this, req.token, mInputMethodState.mTmpExtracted); ims.mChangedStart = EXTRACT_UNKNOWN; ims.mChangedEnd = EXTRACT_UNKNOWN; ims.mChangedDelta = 0; ims.mContentChanged = false; return true; } } } } } return false; } /** * This is used to remove all style-impacting spans from text before new * extracted text is being replaced into it, so that we don't have any * lingering spans applied during the replace. */ static void removeParcelableSpans(Spannable spannable, int start, int end) { Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class); int i = spans.length; while (i > 0) { i--; spannable.removeSpan(spans[i]); } } /** * Apply to this text view the given extracted text, as previously * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}. */ public void setExtractedText(ExtractedText text) { Editable content = getEditableText(); if (text.text != null) { if (content == null) { setText(text.text, TextView.BufferType.EDITABLE); } else if (text.partialStartOffset < 0) { removeParcelableSpans(content, 0, content.length()); content.replace(0, content.length(), text.text); } else { final int N = content.length(); int start = text.partialStartOffset; if (start > N) start = N; int end = text.partialEndOffset; if (end > N) end = N; removeParcelableSpans(content, start, end); content.replace(start, end, text.text); } } // Now set the selection position... make sure it is in range, to // avoid crashes. If this is a partial update, it is possible that // the underlying text may have changed, causing us problems here. // Also we just don't want to trust clients to do the right thing. Spannable sp = (Spannable)getText(); final int N = sp.length(); int start = text.selectionStart; if (start < 0) start = 0; else if (start > N) start = N; int end = text.selectionEnd; if (end < 0) end = 0; else if (end > N) end = N; Selection.setSelection(sp, start, end); // Finally, update the selection mode. if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) { MetaKeyKeyListener.startSelecting(this, sp); } else { MetaKeyKeyListener.stopSelecting(this, sp); } } /** * @hide */ public void setExtracting(ExtractedTextRequest req) { if (mInputMethodState != null) { mInputMethodState.mExtracting = req; } } /** * 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 = Selection.getSelectionStart(mText); if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { registerForPreDraw(); } if (curs >= 0) { mHighlightPathBogus = true; if (isFocused()) { mShowCursor = SystemClock.uptimeMillis(); makeBlink(); } } checkForResize(); } /** * Called by the framework in response to a request to begin a batch * of edit operations through a call to link {@link #beginBatchEdit()}. */ public void onBeginBatchEdit() { } /** * Called by the framework in response to a request to end a batch * of edit operations through a call to link {@link #endBatchEdit}. */ public void onEndBatchEdit() { } /** * Called by the framework in response to a private command from the * current method, provided by it calling * {@link InputConnection#performPrivateCommand * InputConnection.performPrivateCommand()}. * * @param action The action name of the command. * @param data Any additional data for the command. This may be null. * @return Return true if you handled the command, else false. */ public boolean onPrivateIMECommand(String action, Bundle data) { return false; } private void nullLayouts() { if (mLayout instanceof BoringLayout && mSavedLayout == null) { mSavedLayout = (BoringLayout) mLayout; } if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) { mSavedHintLayout = (BoringLayout) mHintLayout; } mLayout = mHintLayout = null; } /** * Make a new Layout based on the already-measured size of the view, * on the assumption that it was measured correctly at some point. */ private void assumeLayout() { int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); if (width < 1) { width = 0; } int physicalWidth = width; if (mHorizontallyScrolling) { width = VERY_WIDE; } makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING, physicalWidth, false); } /** * The width passed in is now the desired layout width, * not the full view width with padding. * {@hide} */ protected void makeNewLayout(int w, int hintWidth, BoringLayout.Metrics boring, BoringLayout.Metrics hintBoring, int ellipsisWidth, boolean bringIntoView) { stopMarquee(); mHighlightPathBogus = true; if (w < 0) { w = 0; } if (hintWidth < 0) { hintWidth = 0; } Layout.Alignment alignment; switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: alignment = Layout.Alignment.ALIGN_CENTER; break; case Gravity.RIGHT: alignment = Layout.Alignment.ALIGN_OPPOSITE; break; default: alignment = Layout.Alignment.ALIGN_NORMAL; } 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); } // Log.e("aaa", "Boring: " + mTransformed); 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); // Log.e("aaa", "Boring but wide: " + mTransformed); } } 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; } } } } 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(); getInterestingRect(r, x, top, bottom, 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. */ public boolean moveCursorToVisibleOffset() { if (!(mText instanceof Spannable)) { return false; } int start = Selection.getSelectionStart(mText); int end = Selection.getSelectionEnd(mText); 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 h, int top, int bottom, int line) { int paddingTop = getExtendedPaddingTop(); if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { paddingTop += getVerticalOffset(false); } top += paddingTop; bottom += paddingTop; h += getCompoundPaddingLeft(); if (line == 0) top -= getExtendedPaddingTop(); if (line == mLayout.getLineCount() - 1) bottom += getExtendedPaddingBottom(); r.set(h, top, h+1, bottom); r.offset(-mScrollX, -mScrollY); } @Override public void debug(int depth) { super.debug(depth); String output = debugIndent(depth); output += "frame={" + mLeft + ", " + mTop + ", " + mRight + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY + "} "; if (mText != null) { output += "mText=\"" + mText + "\" "; if (mLayout != null) { output += "mLayout width=" + mLayout.getWidth() + " height=" + mLayout.getHeight(); } } else { output += "mText=NULL"; } Log.d(VIEW_LOG_TAG, output); } /** * Convenience for {@link Selection#getSelectionStart}. */ @ViewDebug.ExportedProperty public int getSelectionStart() { return Selection.getSelectionStart(getText()); } /** * Convenience for {@link Selection#getSelectionEnd}. */ @ViewDebug.ExportedProperty public int getSelectionEnd() { return Selection.getSelectionEnd(getText()); } /** * Return true iff there is a selection inside this text view. */ public boolean hasSelection() { return getSelectionStart() != getSelectionEnd(); } /** * Sets the properties of this field (lines, horizontally scrolling, * transformation method) to be for a single-line input. * * @attr ref android.R.styleable#TextView_singleLine */ public void setSingleLine() { setSingleLine(true); } /** * If true, sets the properties of this field (lines, horizontally * scrolling, transformation method) to be for a single-line input; * if false, restores these to the default conditions. * Note that calling this with false restores default conditions, * not necessarily those that were in effect prior to calling * it with true. * * @attr ref android.R.styleable#TextView_singleLine */ @android.view.RemotableViewMethod public void setSingleLine(boolean singleLine) { if ((mInputType&EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { if (singleLine) { mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; } else { mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; } } applySingleLine(singleLine, true); } private void applySingleLine(boolean singleLine, boolean applyTransformation) { mSingleLine = singleLine; if (singleLine) { setLines(1); setHorizontallyScrolling(true); if (applyTransformation) { setTransformationMethod(SingleLineTransformationMethod. getInstance()); } } else { setMaxLines(Integer.MAX_VALUE); setHorizontallyScrolling(false); if (applyTransformation) { setTransformationMethod(null); } } } /** * Causes words in the text that are longer than the view is wide * to be ellipsized instead of broken in the middle. You may also * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling} * to constrain the text to a single line. Use <code>null * to turn off ellipsizing. * * @attr ref android.R.styleable#TextView_ellipsize */ public void setEllipsize(TextUtils.TruncateAt where) { mEllipsize = where; if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } /** * Sets how many times to repeat the marquee animation. Only applied if the * TextView has marquee enabled. Set to -1 to repeat indefinitely. * * @attr ref android.R.styleable#TextView_marqueeRepeatLimit */ public void setMarqueeRepeatLimit(int marqueeLimit) { mMarqueeRepeatLimit = marqueeLimit; } /** * Returns where, if anywhere, words that are longer than the view * is wide should be ellipsized. */ @ViewDebug.ExportedProperty public TextUtils.TruncateAt getEllipsize() { return mEllipsize; } /** * Set the TextView so that when it takes focus, all the text is * selected. * * @attr ref android.R.styleable#TextView_selectAllOnFocus */ @android.view.RemotableViewMethod public void setSelectAllOnFocus(boolean selectAllOnFocus) { mSelectAllOnFocus = selectAllOnFocus; if (selectAllOnFocus && !(mText instanceof Spannable)) { setText(mText, BufferType.SPANNABLE); } } /** * Set whether the cursor is visible. The default is true. * * @attr ref android.R.styleable#TextView_cursorVisible */ @android.view.RemotableViewMethod public void setCursorVisible(boolean visible) { mCursorVisible = visible; invalidate(); if (visible) { makeBlink(); } else if (mBlink != null) { mBlink.removeCallbacks(mBlink); } } private boolean canMarquee() { int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()); return width > 0 && mLayout.getLineWidth(0) > width; } private void startMarquee() { // 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 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) / (float) MARQUEE_RESOLUTION; mView = new WeakReference<TextView>(v); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_START: mStatus = MARQUEE_RUNNING; tick(); break; case MESSAGE_TICK: tick(); break; case MESSAGE_RESTART: if (mStatus == MARQUEE_RUNNING) { if (mRepeatLimit >= 0) { mRepeatLimit--; } start(mRepeatLimit); } break; } } void tick() { if (mStatus != MARQUEE_RUNNING) { return; } removeMessages(MESSAGE_TICK); final TextView textView = mView.get(); if (textView != null && (textView.isFocused() || textView.isSelected())) { mScroll += mScrollUnit; if (mScroll > mMaxScroll) { mScroll = mMaxScroll; sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY); } else { sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION); } textView.invalidate(); } } void stop() { mStatus = MARQUEE_STOPPED; removeMessages(MESSAGE_START); removeMessages(MESSAGE_RESTART); removeMessages(MESSAGE_TICK); resetScroll(); } private void resetScroll() { mScroll = 0.0f; final TextView textView = mView.get(); if (textView != null) textView.invalidate(); } void start(int repeatLimit) { if (repeatLimit == 0) { stop(); return; } mRepeatLimit = repeatLimit; final TextView textView = mView.get(); if (textView != null && textView.mLayout != null) { mStatus = MARQUEE_STARTING; mScroll = 0.0f; 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. * If text was replaced with new text or deleted, the * length of the old text was <code>before-start. * @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. * If text was replaced with new text or inserted, * the length of the new text is <code>after-start. */ 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 { ims.mChangedStart = Math.min(ims.mChangedStart, start); ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta); } ims.mChangedDelta += after-before; } sendOnTextChanged(buffer, start, before, after); onTextChanged(buffer, start, before, after); } /** * Not private so it can be called from an inner class without going * through a thunk. */ void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) { // XXX Make the start and end move together if this ends up // spending too much time invalidating. boolean selChanged = false; int newSelStart=-1, newSelEnd=-1; final InputMethodState ims = mInputMethodState; if (what == Selection.SELECTION_END) { mHighlightPathBogus = true; selChanged = true; newSelEnd = newStart; if (!isFocused()) { mSelectionMoved = true; } if (oldStart >= 0 || newStart >= 0) { invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart); registerForPreDraw(); if (isFocused()) { mShowCursor = SystemClock.uptimeMillis(); makeBlink(); } } } if (what == Selection.SELECTION_START) { mHighlightPathBogus = true; selChanged = true; newSelStart = newStart; if (!isFocused()) { mSelectionMoved = true; } if (oldStart >= 0 || newStart >= 0) { int end = Selection.getSelectionEnd(buf); invalidateCursor(end, oldStart, newStart); } } if (selChanged) { if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) { if (newSelStart < 0) { newSelStart = Selection.getSelectionStart(buf); } if (newSelEnd < 0) { newSelEnd = Selection.getSelectionEnd(buf); } onSelectionChanged(newSelStart, newSelEnd); } } if (what instanceof UpdateAppearance || what instanceof ParagraphStyle) { if (ims == null || ims.mBatchEditNesting == 0) { invalidate(); mHighlightPathBogus = true; checkForResize(); } else { ims.mContentChanged = true; } } if (MetaKeyKeyListener.isMetaTracker(buf, what)) { mHighlightPathBogus = true; if (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(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(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(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(TAG, "afterTextChanged: " + buffer); TextView.this.sendAfterTextChanged(buffer); if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) { MetaKeyKeyListener.stopSelecting(TextView.this, buffer); } } public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) { if (DEBUG_EXTRACT) Log.v(TAG, "onSpanChanged s=" + s + " e=" + e + " st=" + st + " en=" + en + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, st, e, en); } public void onSpanAdded(Spannable buf, Object what, int s, int e) { if (DEBUG_EXTRACT) Log.v(TAG, "onSpanAdded s=" + s + " e=" + e + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, -1, s, -1, e); } public void onSpanRemoved(Spannable buf, Object what, int s, int e) { if (DEBUG_EXTRACT) Log.v(TAG, "onSpanRemoved s=" + s + " e=" + e + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, -1, e, -1); } } private void makeBlink() { if (!mCursorVisible) { if (mBlink != null) { mBlink.removeCallbacks(mBlink); } return; } if (mBlink == null) mBlink = new Blink(this); mBlink.removeCallbacks(mBlink); mBlink.postAtTime(mBlink, mShowCursor + BLINK); } /** * @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)) { boolean selMoved = mSelectionMoved; if (mMovement != null) { mMovement.onTakeFocus(this, (Spannable) mText, direction); } if (mSelectAllOnFocus) { Selection.setSelection((Spannable) mText, 0, mText.length()); } if (selMoved && selStart >= 0 && selEnd >= 0) { /* * Someone intentionally set the selection, so let them * do whatever it is that they wanted to do instead of * the default on-focus behavior. We reset the selection * here instead of just skipping the onTakeFocus() call * because some movement methods do something other than * just setting the selection in theirs and we still * need to go through that path. */ Selection.setSelection((Spannable) mText, selStart, selEnd); } 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(); } 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(); } } } class CommitSelectionReceiver extends ResultReceiver { int mNewStart; int mNewEnd; CommitSelectionReceiver() { super(getHandler()); } protected void onReceiveResult(int resultCode, Bundle resultData) { if (resultCode != InputMethodManager.RESULT_SHOWN) { final int len = mText.length(); if (mNewStart > len) { mNewStart = len; } if (mNewEnd > len) { mNewEnd = len; } Selection.setSelection((Spannable)mText, mNewStart, mNewEnd); } } } @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getAction(); 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) { boolean handled = false; int oldSelStart = Selection.getSelectionStart(mText); int oldSelEnd = Selection.getSelectionEnd(mText); if (mMovement != null) { handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); } if (mText instanceof Editable && onCheckIsTextEditor()) { if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); // This is going to be gross... if tapping on the text view // causes the IME to be displayed, we don't want the selection // to change. But the selection has already changed, and // we won't know right away whether the IME is getting // displayed, so... int newSelStart = Selection.getSelectionStart(mText); int newSelEnd = Selection.getSelectionEnd(mText); CommitSelectionReceiver csr = null; if (newSelStart != oldSelStart || newSelEnd != oldSelEnd) { csr = new CommitSelectionReceiver(); csr.mNewStart = newSelStart; csr.mNewEnd = newSelEnd; } if (imm.showSoftInput(this, 0, csr) && csr != null) { // The IME might get shown -- revert to the old // selection, and change to the new when we finally // find out of it is okay. Selection.setSelection((Spannable)mText, oldSelStart, oldSelEnd); handled = true; } } } if (handled) { return true; } } return superResult; } /** * 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 WeakReference<TextView> mView; private boolean mCancelled; public Blink(TextView v) { mView = new WeakReference<TextView>(v); } public void run() { if (mCancelled) { return; } removeCallbacks(Blink.this); TextView tv = mView.get(); if (tv != null && tv.isFocused()) { int st = Selection.getSelectionStart(tv.mText); int en = Selection.getSelectionEnd(tv.mText); if (st == en && st >= 0 && en >= 0) { if (tv.mLayout != null) { tv.invalidateCursorPath(); } postAtTime(this, SystemClock.uptimeMillis() + BLINK); } } } void cancel() { if (!mCancelled) { removeCallbacks(Blink.this); mCancelled = true; } } void uncancel() { mCancelled = false; } } @Override protected float getLeftFadingEdgeStrength() { if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { if (mMarquee != null && !mMarquee.isStopped()) { final Marquee marquee = mMarquee; 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() { if (mText instanceof Spannable && mText.length() != 0 && mMovement != null && mMovement.canSelectArbitrarily()) { return true; } return false; } private boolean canSelectText() { if (mText instanceof Spannable && mText.length() != 0 && mMovement != null && mMovement.canSelectArbitrarily()) { return true; } return false; } private boolean canCut() { if (mTransformation instanceof PasswordTransformationMethod) { return false; } if (mText.length() > 0 && getSelectionStart() >= 0) { if (mText instanceof Editable && mInput != null) { return true; } } return false; } private boolean canCopy() { if (mTransformation instanceof PasswordTransformationMethod) { return false; } if (mText.length() > 0 && getSelectionStart() >= 0) { return true; } return false; } private boolean canPaste() { if (mText instanceof Editable && mInput != null && getSelectionStart() >= 0 && getSelectionEnd() >= 0) { ClipboardManager clip = (ClipboardManager)getContext() .getSystemService(Context.CLIPBOARD_SERVICE); if (clip.hasText()) { return true; } } return false; } /** * Returns a word to add to the dictionary from the context menu, * or null if there is no cursor or no word at the cursor. */ private String getWordForDictionary() { /* * 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 null; } 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 null; } int end = getSelectionEnd(); if (end < 0) { return null; } int start = end; int len = mText.length(); for (; start > 0; start--) { char c = mTransformed.charAt(start - 1); int type = Character.getType(c); if (c != '\'' && type != Character.UPPERCASE_LETTER && type != Character.LOWERCASE_LETTER && type != Character.TITLECASE_LETTER && type != Character.MODIFIER_LETTER && type != Character.DECIMAL_DIGIT_NUMBER) { break; } } for (; end < len; end++) { char c = mTransformed.charAt(end); int type = Character.getType(c); if (c != '\'' && type != Character.UPPERCASE_LETTER && type != Character.LOWERCASE_LETTER && type != Character.TITLECASE_LETTER && type != Character.MODIFIER_LETTER && type != Character.DECIMAL_DIGIT_NUMBER) { break; } } boolean hasLetter = false; for (int i = start; i < end; i++) { if (Character.isLetter(mTransformed.charAt(i))) { hasLetter = true; break; } } if (!hasLetter) { return null; } if (start == end) { return null; } if (end - start > 48) { return null; } return TextUtils.substring(mTransformed, start, end); } @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 (!isFocused()) { if (isFocusable() && mInput != null) { if (canCopy()) { MenuHandler handler = new MenuHandler(); int name = com.android.internal.R.string.copyAll; menu.add(0, ID_COPY, 0, name). setOnMenuItemClickListener(handler). setAlphabeticShortcut('c'); menu.setHeaderTitle(com.android.internal.R.string. editTextMenuTitle); } } return; } MenuHandler handler = new MenuHandler(); if (canSelectAll()) { menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). setOnMenuItemClickListener(handler). setAlphabeticShortcut('a'); added = true; } boolean selection = getSelectionStart() != getSelectionEnd(); if (canSelectText()) { if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) { menu.add(0, ID_STOP_SELECTING_TEXT, 0, com.android.internal.R.string.stopSelectingText). setOnMenuItemClickListener(handler); added = true; } else { menu.add(0, ID_START_SELECTING_TEXT, 0, com.android.internal.R.string.selectText). setOnMenuItemClickListener(handler); added = true; } } if (canCut()) { int name; if (selection) { name = com.android.internal.R.string.cut; } else { name = com.android.internal.R.string.cutAll; } menu.add(0, ID_CUT, 0, name). setOnMenuItemClickListener(handler). setAlphabeticShortcut('x'); added = true; } if (canCopy()) { int name; if (selection) { name = com.android.internal.R.string.copy; } else { name = com.android.internal.R.string.copyAll; } menu.add(0, ID_COPY, 0, name). setOnMenuItemClickListener(handler). setAlphabeticShortcut('c'); added = true; } if (canPaste()) { menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). setOnMenuItemClickListener(handler). setAlphabeticShortcut('v'); added = true; } if (mText instanceof Spanned) { int selStart = getSelectionStart(); int selEnd = getSelectionEnd(); int min = Math.min(selStart, selEnd); int max = Math.max(selStart, selEnd); URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class); if (urls.length == 1) { menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl). setOnMenuItemClickListener(handler); added = true; } } if (isInputMethodTarget()) { menu.add(1, ID_SWITCH_INPUT_METHOD, 0, com.android.internal.R.string.inputMethod). setOnMenuItemClickListener(handler); added = true; } String word = getWordForDictionary(); if (word != null) { menu.add(1, ID_ADD_TO_DICTIONARY, 0, getContext().getString(com.android.internal.R.string.addToDictionary, word)). setOnMenuItemClickListener(handler); added = true; } if (added) { menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle); } } /** * Returns whether this text view is a current input method target. The * default implementation just checks with {@link InputMethodManager}. */ public boolean isInputMethodTarget() { InputMethodManager imm = InputMethodManager.peekInstance(); return imm != null && imm.isActive(this); } private static final int ID_SELECT_ALL = android.R.id.selectAll; private static final int ID_START_SELECTING_TEXT = android.R.id.startSelectingText; private static final int ID_STOP_SELECTING_TEXT = android.R.id.stopSelectingText; private static final int ID_CUT = android.R.id.cut; private static final int ID_COPY = android.R.id.copy; private static final int ID_PASTE = android.R.id.paste; private static final int ID_COPY_URL = android.R.id.copyUrl; private static final int ID_SWITCH_INPUT_METHOD = android.R.id.switchInputMethod; private static final int ID_ADD_TO_DICTIONARY = android.R.id.addToDictionary; private class MenuHandler implements MenuItem.OnMenuItemClickListener { public boolean onMenuItemClick(MenuItem item) { return onTextContextMenuItem(item.getItemId()); } } /** * Called when a context menu option for the text view is selected. Currently * this will be one of: {@link android.R.id#selectAll}, * {@link android.R.id#startSelectingText}, {@link android.R.id#stopSelectingText}, * {@link android.R.id#cut}, {@link android.R.id#copy}, * {@link android.R.id#paste}, {@link android.R.id#copyUrl}, * or {@link android.R.id#switchInputMethod}. */ public boolean onTextContextMenuItem(int id) { int selStart = getSelectionStart(); int selEnd = getSelectionEnd(); if (!isFocused()) { selStart = 0; selEnd = mText.length(); } int min = Math.min(selStart, selEnd); int max = Math.max(selStart, selEnd); if (min < 0) { min = 0; } if (max < 0) { max = 0; } ClipboardManager clip = (ClipboardManager)getContext() .getSystemService(Context.CLIPBOARD_SERVICE); switch (id) { case ID_SELECT_ALL: Selection.setSelection((Spannable) mText, 0, mText.length()); return true; case ID_START_SELECTING_TEXT: MetaKeyKeyListener.startSelecting(this, (Spannable) mText); return true; case ID_STOP_SELECTING_TEXT: MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); Selection.setSelection((Spannable) mText, getSelectionEnd()); return true; case ID_CUT: MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); if (min == max) { min = 0; max = mText.length(); } clip.setText(mTransformed.subSequence(min, max)); ((Editable) mText).delete(min, max); return true; case ID_COPY: MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); if (min == max) { min = 0; max = mText.length(); } clip.setText(mTransformed.subSequence(min, max)); return true; case ID_PASTE: MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); CharSequence paste = clip.getText(); if (paste != null) { Selection.setSelection((Spannable) mText, max); ((Editable) mText).replace(min, max, paste); } return true; case ID_COPY_URL: MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class); if (urls.length == 1) { clip.setText(urls[0].getURL()); } return true; case ID_SWITCH_INPUT_METHOD: InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { imm.showInputMethodPicker(); } return true; case ID_ADD_TO_DICTIONARY: String word = getWordForDictionary(); if (word != null) { Intent i = new Intent("com.android.settings.USER_DICTIONARY_INSERT"); i.putExtra("word", word); i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(i); } return true; } return false; } public boolean performLongClick() { if (super.performLongClick()) { mEatTouchRelease = true; return true; } return false; } @ViewDebug.ExportedProperty private CharSequence mText; private CharSequence mTransformed; private BufferType mBufferType = BufferType.NORMAL; private int mInputType = EditorInfo.TYPE_NULL; private CharSequence mHint; private Layout mHintLayout; private KeyListener mInput; private MovementMethod mMovement; private TransformationMethod mTransformation; private ChangeWatcher mChangeWatcher; private ArrayList<TextWatcher> mListeners = null; // display attributes private TextPaint mTextPaint; private boolean mUserSetTextScaleX; private Paint mHighlightPaint; private int mHighlightColor = 0xFFBBDDFF; private Layout mLayout; private long mShowCursor; private Blink mBlink; private boolean mCursorVisible = true; private boolean mSelectAllOnFocus = false; private int mGravity = Gravity.TOP | Gravity.LEFT; private boolean mHorizontallyScrolling; private int mAutoLinkMask; private boolean mLinksClickable = true; private float mSpacingMult = 1; private float mSpacingAdd = 0; private static final int LINES = 1; private static final int EMS = LINES; private static final int PIXELS = 2; private int mMaximum = Integer.MAX_VALUE; private int mMaxMode = LINES; private int mMinimum = 0; private int mMinMode = LINES; private int mMaxWidth = Integer.MAX_VALUE; private int mMaxWidthMode = PIXELS; private int mMinWidth = 0; private int mMinWidthMode = PIXELS; private boolean mSingleLine; private int mDesiredHeightAtMeasure = -1; private boolean mIncludePad = true; // tmp primitives, so we don't alloc them on each draw private Path mHighlightPath; private boolean mHighlightPathBogus = true; private static final RectF sTempRect = new RectF(); // XXX should be much larger private static final int VERY_WIDE = 16384; private static final int BLINK = 500; private static final int ANIMATED_SCROLL_GAP = 250; private long mLastScroll; private Scroller mScroller = null; private BoringLayout.Metrics mBoring; private BoringLayout.Metrics mHintBoring; private BoringLayout mSavedLayout, mSavedHintLayout; private static final InputFilter[] NO_FILTERS = new InputFilter[0]; private InputFilter[] mFilters = NO_FILTERS; private static final Spanned EMPTY_SPANNED = new SpannedString(""); }

Other Android examples (source code examples)

Here is a short list of links related to this Android TextView.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.