• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.R;
20 import android.content.ClipData;
21 import android.content.ClipData.Item;
22 import android.content.ClipboardManager;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.PackageManager;
26 import android.content.res.ColorStateList;
27 import android.content.res.Resources;
28 import android.content.res.TypedArray;
29 import android.content.res.XmlResourceParser;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.Paint;
33 import android.graphics.Path;
34 import android.graphics.Rect;
35 import android.graphics.RectF;
36 import android.graphics.Typeface;
37 import android.graphics.drawable.Drawable;
38 import android.inputmethodservice.ExtractEditText;
39 import android.net.Uri;
40 import android.os.Bundle;
41 import android.os.Handler;
42 import android.os.Message;
43 import android.os.Parcel;
44 import android.os.Parcelable;
45 import android.os.SystemClock;
46 import android.provider.Settings;
47 import android.text.BoringLayout;
48 import android.text.DynamicLayout;
49 import android.text.Editable;
50 import android.text.GetChars;
51 import android.text.GraphicsOperations;
52 import android.text.InputFilter;
53 import android.text.InputType;
54 import android.text.Layout;
55 import android.text.ParcelableSpan;
56 import android.text.Selection;
57 import android.text.SpanWatcher;
58 import android.text.Spannable;
59 import android.text.SpannableString;
60 import android.text.SpannableStringBuilder;
61 import android.text.Spanned;
62 import android.text.SpannedString;
63 import android.text.StaticLayout;
64 import android.text.TextDirectionHeuristic;
65 import android.text.TextDirectionHeuristics;
66 import android.text.TextPaint;
67 import android.text.TextUtils;
68 import android.text.TextUtils.TruncateAt;
69 import android.text.TextWatcher;
70 import android.text.method.AllCapsTransformationMethod;
71 import android.text.method.ArrowKeyMovementMethod;
72 import android.text.method.DateKeyListener;
73 import android.text.method.DateTimeKeyListener;
74 import android.text.method.DialerKeyListener;
75 import android.text.method.DigitsKeyListener;
76 import android.text.method.KeyListener;
77 import android.text.method.LinkMovementMethod;
78 import android.text.method.MetaKeyKeyListener;
79 import android.text.method.MovementMethod;
80 import android.text.method.PasswordTransformationMethod;
81 import android.text.method.SingleLineTransformationMethod;
82 import android.text.method.TextKeyListener;
83 import android.text.method.TimeKeyListener;
84 import android.text.method.TransformationMethod;
85 import android.text.method.TransformationMethod2;
86 import android.text.method.WordIterator;
87 import android.text.style.ClickableSpan;
88 import android.text.style.EasyEditSpan;
89 import android.text.style.ParagraphStyle;
90 import android.text.style.SpellCheckSpan;
91 import android.text.style.SuggestionRangeSpan;
92 import android.text.style.SuggestionSpan;
93 import android.text.style.TextAppearanceSpan;
94 import android.text.style.URLSpan;
95 import android.text.style.UpdateAppearance;
96 import android.text.util.Linkify;
97 import android.util.AttributeSet;
98 import android.util.DisplayMetrics;
99 import android.util.FloatMath;
100 import android.util.Log;
101 import android.util.TypedValue;
102 import android.view.ActionMode;
103 import android.view.ActionMode.Callback;
104 import android.view.ContextMenu;
105 import android.view.DragEvent;
106 import android.view.Gravity;
107 import android.view.HapticFeedbackConstants;
108 import android.view.KeyCharacterMap;
109 import android.view.KeyEvent;
110 import android.view.LayoutInflater;
111 import android.view.Menu;
112 import android.view.MenuItem;
113 import android.view.MotionEvent;
114 import android.view.View;
115 import android.view.ViewConfiguration;
116 import android.view.ViewDebug;
117 import android.view.ViewGroup;
118 import android.view.ViewGroup.LayoutParams;
119 import android.view.ViewParent;
120 import android.view.ViewRootImpl;
121 import android.view.ViewTreeObserver;
122 import android.view.WindowManager;
123 import android.view.accessibility.AccessibilityEvent;
124 import android.view.accessibility.AccessibilityManager;
125 import android.view.accessibility.AccessibilityNodeInfo;
126 import android.view.animation.AnimationUtils;
127 import android.view.inputmethod.BaseInputConnection;
128 import android.view.inputmethod.CompletionInfo;
129 import android.view.inputmethod.CorrectionInfo;
130 import android.view.inputmethod.EditorInfo;
131 import android.view.inputmethod.ExtractedText;
132 import android.view.inputmethod.ExtractedTextRequest;
133 import android.view.inputmethod.InputConnection;
134 import android.view.inputmethod.InputMethodManager;
135 import android.widget.AdapterView.OnItemClickListener;
136 import android.widget.RemoteViews.RemoteView;
137 
138 import com.android.internal.util.FastMath;
139 import com.android.internal.widget.EditableInputConnection;
140 
141 import org.xmlpull.v1.XmlPullParserException;
142 
143 import java.io.IOException;
144 import java.lang.ref.WeakReference;
145 import java.text.BreakIterator;
146 import java.util.ArrayList;
147 import java.util.Arrays;
148 import java.util.Comparator;
149 import java.util.HashMap;
150 
151 /**
152  * Displays text to the user and optionally allows them to edit it.  A TextView
153  * is a complete text editor, however the basic class is configured to not
154  * allow editing; see {@link EditText} for a subclass that configures the text
155  * view for editing.
156  *
157  * <p>
158  * <b>XML attributes</b>
159  * <p>
160  * See {@link android.R.styleable#TextView TextView Attributes},
161  * {@link android.R.styleable#View View Attributes}
162  *
163  * @attr ref android.R.styleable#TextView_text
164  * @attr ref android.R.styleable#TextView_bufferType
165  * @attr ref android.R.styleable#TextView_hint
166  * @attr ref android.R.styleable#TextView_textColor
167  * @attr ref android.R.styleable#TextView_textColorHighlight
168  * @attr ref android.R.styleable#TextView_textColorHint
169  * @attr ref android.R.styleable#TextView_textAppearance
170  * @attr ref android.R.styleable#TextView_textColorLink
171  * @attr ref android.R.styleable#TextView_textSize
172  * @attr ref android.R.styleable#TextView_textScaleX
173  * @attr ref android.R.styleable#TextView_typeface
174  * @attr ref android.R.styleable#TextView_textStyle
175  * @attr ref android.R.styleable#TextView_cursorVisible
176  * @attr ref android.R.styleable#TextView_maxLines
177  * @attr ref android.R.styleable#TextView_maxHeight
178  * @attr ref android.R.styleable#TextView_lines
179  * @attr ref android.R.styleable#TextView_height
180  * @attr ref android.R.styleable#TextView_minLines
181  * @attr ref android.R.styleable#TextView_minHeight
182  * @attr ref android.R.styleable#TextView_maxEms
183  * @attr ref android.R.styleable#TextView_maxWidth
184  * @attr ref android.R.styleable#TextView_ems
185  * @attr ref android.R.styleable#TextView_width
186  * @attr ref android.R.styleable#TextView_minEms
187  * @attr ref android.R.styleable#TextView_minWidth
188  * @attr ref android.R.styleable#TextView_gravity
189  * @attr ref android.R.styleable#TextView_scrollHorizontally
190  * @attr ref android.R.styleable#TextView_password
191  * @attr ref android.R.styleable#TextView_singleLine
192  * @attr ref android.R.styleable#TextView_selectAllOnFocus
193  * @attr ref android.R.styleable#TextView_includeFontPadding
194  * @attr ref android.R.styleable#TextView_maxLength
195  * @attr ref android.R.styleable#TextView_shadowColor
196  * @attr ref android.R.styleable#TextView_shadowDx
197  * @attr ref android.R.styleable#TextView_shadowDy
198  * @attr ref android.R.styleable#TextView_shadowRadius
199  * @attr ref android.R.styleable#TextView_autoLink
200  * @attr ref android.R.styleable#TextView_linksClickable
201  * @attr ref android.R.styleable#TextView_numeric
202  * @attr ref android.R.styleable#TextView_digits
203  * @attr ref android.R.styleable#TextView_phoneNumber
204  * @attr ref android.R.styleable#TextView_inputMethod
205  * @attr ref android.R.styleable#TextView_capitalize
206  * @attr ref android.R.styleable#TextView_autoText
207  * @attr ref android.R.styleable#TextView_editable
208  * @attr ref android.R.styleable#TextView_freezesText
209  * @attr ref android.R.styleable#TextView_ellipsize
210  * @attr ref android.R.styleable#TextView_drawableTop
211  * @attr ref android.R.styleable#TextView_drawableBottom
212  * @attr ref android.R.styleable#TextView_drawableRight
213  * @attr ref android.R.styleable#TextView_drawableLeft
214  * @attr ref android.R.styleable#TextView_drawablePadding
215  * @attr ref android.R.styleable#TextView_lineSpacingExtra
216  * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
217  * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
218  * @attr ref android.R.styleable#TextView_inputType
219  * @attr ref android.R.styleable#TextView_imeOptions
220  * @attr ref android.R.styleable#TextView_privateImeOptions
221  * @attr ref android.R.styleable#TextView_imeActionLabel
222  * @attr ref android.R.styleable#TextView_imeActionId
223  * @attr ref android.R.styleable#TextView_editorExtras
224  */
225 @RemoteView
226 public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
227     static final String LOG_TAG = "TextView";
228     static final boolean DEBUG_EXTRACT = false;
229 
230     private static final int PRIORITY = 100;
231     private int mCurrentAlpha = 255;
232 
233     final int[] mTempCoords = new int[2];
234     Rect mTempRect;
235 
236     private ColorStateList mTextColor;
237     private int mCurTextColor;
238     private ColorStateList mHintTextColor;
239     private ColorStateList mLinkTextColor;
240     private int mCurHintTextColor;
241     private boolean mFreezesText;
242     private boolean mFrozenWithFocus;
243     private boolean mTemporaryDetach;
244     private boolean mDispatchTemporaryDetach;
245 
246     private boolean mDiscardNextActionUp = false;
247     private boolean mIgnoreActionUpEvent = false;
248 
249     private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
250     private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
251 
252     private float mShadowRadius, mShadowDx, mShadowDy;
253 
254     private static final int PREDRAW_NOT_REGISTERED = 0;
255     private static final int PREDRAW_PENDING = 1;
256     private static final int PREDRAW_DONE = 2;
257     private int mPreDrawState = PREDRAW_NOT_REGISTERED;
258 
259     private TextUtils.TruncateAt mEllipsize = null;
260 
261     // Enum for the "typeface" XML parameter.
262     // TODO: How can we get this from the XML instead of hardcoding it here?
263     private static final int SANS = 1;
264     private static final int SERIF = 2;
265     private static final int MONOSPACE = 3;
266 
267     // Bitfield for the "numeric" XML parameter.
268     // TODO: How can we get this from the XML instead of hardcoding it here?
269     private static final int SIGNED = 2;
270     private static final int DECIMAL = 4;
271 
272     class Drawables {
273         final Rect mCompoundRect = new Rect();
274         Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight,
275                 mDrawableStart, mDrawableEnd;
276         int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight,
277                 mDrawableSizeStart, mDrawableSizeEnd;
278         int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight,
279                 mDrawableHeightStart, mDrawableHeightEnd;
280         int mDrawablePadding;
281     }
282     private Drawables mDrawables;
283 
284     private CharSequence mError;
285     private boolean mErrorWasChanged;
286     private ErrorPopup mPopup;
287     /**
288      * This flag is set if the TextView tries to display an error before it
289      * is attached to the window (so its position is still unknown).
290      * It causes the error to be shown later, when onAttachedToWindow()
291      * is called.
292      */
293     private boolean mShowErrorAfterAttach;
294 
295     private CharWrapper mCharWrapper = null;
296 
297     private boolean mSelectionMoved = false;
298     private boolean mTouchFocusSelected = false;
299 
300     private Marquee mMarquee;
301     private boolean mRestartMarquee;
302 
303     private int mMarqueeRepeatLimit = 3;
304 
305     class InputContentType {
306         int imeOptions = EditorInfo.IME_NULL;
307         String privateImeOptions;
308         CharSequence imeActionLabel;
309         int imeActionId;
310         Bundle extras;
311         OnEditorActionListener onEditorActionListener;
312         boolean enterDown;
313     }
314     InputContentType mInputContentType;
315 
316     class InputMethodState {
317         Rect mCursorRectInWindow = new Rect();
318         RectF mTmpRectF = new RectF();
319         float[] mTmpOffset = new float[2];
320         ExtractedTextRequest mExtracting;
321         final ExtractedText mTmpExtracted = new ExtractedText();
322         int mBatchEditNesting;
323         boolean mCursorChanged;
324         boolean mSelectionModeChanged;
325         boolean mContentChanged;
326         int mChangedStart, mChangedEnd, mChangedDelta;
327     }
328     InputMethodState mInputMethodState;
329 
330     private int mTextSelectHandleLeftRes;
331     private int mTextSelectHandleRightRes;
332     private int mTextSelectHandleRes;
333 
334     private int mTextEditSuggestionItemLayout;
335     private SuggestionsPopupWindow mSuggestionsPopupWindow;
336     private SuggestionRangeSpan mSuggestionRangeSpan;
337 
338     private int mCursorDrawableRes;
339     private final Drawable[] mCursorDrawable = new Drawable[2];
340     private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2
341 
342     private Drawable mSelectHandleLeft;
343     private Drawable mSelectHandleRight;
344     private Drawable mSelectHandleCenter;
345 
346     // Global listener that detects changes in the global position of the TextView
347     private PositionListener mPositionListener;
348 
349     private float mLastDownPositionX, mLastDownPositionY;
350     private Callback mCustomSelectionActionModeCallback;
351 
352     private final int mSquaredTouchSlopDistance;
353     // Set when this TextView gained focus with some text selected. Will start selection mode.
354     private boolean mCreatedWithASelection = false;
355 
356     private WordIterator mWordIterator;
357 
358     private SpellChecker mSpellChecker;
359 
360     // The alignment to pass to Layout, or null if not resolved.
361     private Layout.Alignment mLayoutAlignment;
362 
363     // The default value for mTextAlign.
364     private TextAlign mTextAlign = TextAlign.INHERIT;
365 
366     private static enum TextAlign {
367         INHERIT, GRAVITY, TEXT_START, TEXT_END, CENTER, VIEW_START, VIEW_END;
368     }
369 
370     private boolean mResolvedDrawables = false;
371 
372     /**
373      * On some devices the fading edges add a performance penalty if used
374      * extensively in the same layout. This mode indicates how the marquee
375      * is currently being shown, if applicable. (mEllipsize will == MARQUEE)
376      */
377     private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
378 
379     /**
380      * When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores
381      * the layout that should be used when the mode switches.
382      */
383     private Layout mSavedMarqueeModeLayout;
384 
385     /**
386      * Draw marquee text with fading edges as usual
387      */
388     private static final int MARQUEE_FADE_NORMAL = 0;
389 
390     /**
391      * Draw marquee text as ellipsize end while inactive instead of with the fade.
392      * (Useful for devices where the fade can be expensive if overdone)
393      */
394     private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1;
395 
396     /**
397      * Draw marquee text with fading edges because it is currently active/animating.
398      */
399     private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2;
400 
401     /*
402      * Kick-start the font cache for the zygote process (to pay the cost of
403      * initializing freetype for our default font only once).
404      */
405     static {
406         Paint p = new Paint();
407         p.setAntiAlias(true);
408         // We don't care about the result, just the side-effect of measuring.
409         p.measureText("H");
410     }
411 
412     /**
413      * Interface definition for a callback to be invoked when an action is
414      * performed on the editor.
415      */
416     public interface OnEditorActionListener {
417         /**
418          * Called when an action is being performed.
419          *
420          * @param v The view that was clicked.
421          * @param actionId Identifier of the action.  This will be either the
422          * identifier you supplied, or {@link EditorInfo#IME_NULL
423          * EditorInfo.IME_NULL} if being called due to the enter key
424          * being pressed.
425          * @param event If triggered by an enter key, this is the event;
426          * otherwise, this is null.
427          * @return Return true if you have consumed the action, else false.
428          */
onEditorAction(TextView v, int actionId, KeyEvent event)429         boolean onEditorAction(TextView v, int actionId, KeyEvent event);
430     }
431 
TextView(Context context)432     public TextView(Context context) {
433         this(context, null);
434     }
435 
TextView(Context context, AttributeSet attrs)436     public TextView(Context context,
437                     AttributeSet attrs) {
438         this(context, attrs, com.android.internal.R.attr.textViewStyle);
439     }
440 
441     @SuppressWarnings("deprecation")
TextView(Context context, AttributeSet attrs, int defStyle)442     public TextView(Context context,
443                     AttributeSet attrs,
444                     int defStyle) {
445         super(context, attrs, defStyle);
446         mText = "";
447 
448         mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
449         mTextPaint.density = getResources().getDisplayMetrics().density;
450         mTextPaint.setCompatibilityScaling(
451                 getResources().getCompatibilityInfo().applicationScale);
452 
453         // If we get the paint from the skin, we should set it to left, since
454         // the layout always wants it to be left.
455         // mTextPaint.setTextAlign(Paint.Align.LEFT);
456 
457         mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
458         mHighlightPaint.setCompatibilityScaling(
459                 getResources().getCompatibilityInfo().applicationScale);
460 
461         mMovement = getDefaultMovementMethod();
462         mTransformation = null;
463 
464         int textColorHighlight = 0;
465         ColorStateList textColor = null;
466         ColorStateList textColorHint = null;
467         ColorStateList textColorLink = null;
468         int textSize = 15;
469         int typefaceIndex = -1;
470         int styleIndex = -1;
471         boolean allCaps = false;
472 
473         final Resources.Theme theme = context.getTheme();
474 
475         /*
476          * Look the appearance up without checking first if it exists because
477          * almost every TextView has one and it greatly simplifies the logic
478          * to be able to parse the appearance first and then let specific tags
479          * for this View override it.
480          */
481         TypedArray a = theme.obtainStyledAttributes(
482                     attrs, com.android.internal.R.styleable.TextViewAppearance, defStyle, 0);
483         TypedArray appearance = null;
484         int ap = a.getResourceId(
485                 com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
486         a.recycle();
487         if (ap != -1) {
488             appearance = theme.obtainStyledAttributes(
489                     ap, com.android.internal.R.styleable.TextAppearance);
490         }
491         if (appearance != null) {
492             int n = appearance.getIndexCount();
493             for (int i = 0; i < n; i++) {
494                 int attr = appearance.getIndex(i);
495 
496                 switch (attr) {
497                 case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
498                     textColorHighlight = appearance.getColor(attr, textColorHighlight);
499                     break;
500 
501                 case com.android.internal.R.styleable.TextAppearance_textColor:
502                     textColor = appearance.getColorStateList(attr);
503                     break;
504 
505                 case com.android.internal.R.styleable.TextAppearance_textColorHint:
506                     textColorHint = appearance.getColorStateList(attr);
507                     break;
508 
509                 case com.android.internal.R.styleable.TextAppearance_textColorLink:
510                     textColorLink = appearance.getColorStateList(attr);
511                     break;
512 
513                 case com.android.internal.R.styleable.TextAppearance_textSize:
514                     textSize = appearance.getDimensionPixelSize(attr, textSize);
515                     break;
516 
517                 case com.android.internal.R.styleable.TextAppearance_typeface:
518                     typefaceIndex = appearance.getInt(attr, -1);
519                     break;
520 
521                 case com.android.internal.R.styleable.TextAppearance_textStyle:
522                     styleIndex = appearance.getInt(attr, -1);
523                     break;
524 
525                 case com.android.internal.R.styleable.TextAppearance_textAllCaps:
526                     allCaps = appearance.getBoolean(attr, false);
527                     break;
528                 }
529             }
530 
531             appearance.recycle();
532         }
533 
534         boolean editable = getDefaultEditable();
535         CharSequence inputMethod = null;
536         int numeric = 0;
537         CharSequence digits = null;
538         boolean phone = false;
539         boolean autotext = false;
540         int autocap = -1;
541         int buffertype = 0;
542         boolean selectallonfocus = false;
543         Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
544             drawableBottom = null, drawableStart = null, drawableEnd = null;
545         int drawablePadding = 0;
546         int ellipsize = -1;
547         boolean singleLine = false;
548         int maxlength = -1;
549         CharSequence text = "";
550         CharSequence hint = null;
551         int shadowcolor = 0;
552         float dx = 0, dy = 0, r = 0;
553         boolean password = false;
554         int inputType = EditorInfo.TYPE_NULL;
555 
556         a = theme.obtainStyledAttributes(
557                     attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
558 
559         int n = a.getIndexCount();
560         for (int i = 0; i < n; i++) {
561             int attr = a.getIndex(i);
562 
563             switch (attr) {
564             case com.android.internal.R.styleable.TextView_editable:
565                 editable = a.getBoolean(attr, editable);
566                 break;
567 
568             case com.android.internal.R.styleable.TextView_inputMethod:
569                 inputMethod = a.getText(attr);
570                 break;
571 
572             case com.android.internal.R.styleable.TextView_numeric:
573                 numeric = a.getInt(attr, numeric);
574                 break;
575 
576             case com.android.internal.R.styleable.TextView_digits:
577                 digits = a.getText(attr);
578                 break;
579 
580             case com.android.internal.R.styleable.TextView_phoneNumber:
581                 phone = a.getBoolean(attr, phone);
582                 break;
583 
584             case com.android.internal.R.styleable.TextView_autoText:
585                 autotext = a.getBoolean(attr, autotext);
586                 break;
587 
588             case com.android.internal.R.styleable.TextView_capitalize:
589                 autocap = a.getInt(attr, autocap);
590                 break;
591 
592             case com.android.internal.R.styleable.TextView_bufferType:
593                 buffertype = a.getInt(attr, buffertype);
594                 break;
595 
596             case com.android.internal.R.styleable.TextView_selectAllOnFocus:
597                 selectallonfocus = a.getBoolean(attr, selectallonfocus);
598                 break;
599 
600             case com.android.internal.R.styleable.TextView_autoLink:
601                 mAutoLinkMask = a.getInt(attr, 0);
602                 break;
603 
604             case com.android.internal.R.styleable.TextView_linksClickable:
605                 mLinksClickable = a.getBoolean(attr, true);
606                 break;
607 
608             case com.android.internal.R.styleable.TextView_drawableLeft:
609                 drawableLeft = a.getDrawable(attr);
610                 break;
611 
612             case com.android.internal.R.styleable.TextView_drawableTop:
613                 drawableTop = a.getDrawable(attr);
614                 break;
615 
616             case com.android.internal.R.styleable.TextView_drawableRight:
617                 drawableRight = a.getDrawable(attr);
618                 break;
619 
620             case com.android.internal.R.styleable.TextView_drawableBottom:
621                 drawableBottom = a.getDrawable(attr);
622                 break;
623 
624             case com.android.internal.R.styleable.TextView_drawableStart:
625                 drawableStart = a.getDrawable(attr);
626                 break;
627 
628             case com.android.internal.R.styleable.TextView_drawableEnd:
629                 drawableEnd = a.getDrawable(attr);
630                 break;
631 
632             case com.android.internal.R.styleable.TextView_drawablePadding:
633                 drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
634                 break;
635 
636             case com.android.internal.R.styleable.TextView_maxLines:
637                 setMaxLines(a.getInt(attr, -1));
638                 break;
639 
640             case com.android.internal.R.styleable.TextView_maxHeight:
641                 setMaxHeight(a.getDimensionPixelSize(attr, -1));
642                 break;
643 
644             case com.android.internal.R.styleable.TextView_lines:
645                 setLines(a.getInt(attr, -1));
646                 break;
647 
648             case com.android.internal.R.styleable.TextView_height:
649                 setHeight(a.getDimensionPixelSize(attr, -1));
650                 break;
651 
652             case com.android.internal.R.styleable.TextView_minLines:
653                 setMinLines(a.getInt(attr, -1));
654                 break;
655 
656             case com.android.internal.R.styleable.TextView_minHeight:
657                 setMinHeight(a.getDimensionPixelSize(attr, -1));
658                 break;
659 
660             case com.android.internal.R.styleable.TextView_maxEms:
661                 setMaxEms(a.getInt(attr, -1));
662                 break;
663 
664             case com.android.internal.R.styleable.TextView_maxWidth:
665                 setMaxWidth(a.getDimensionPixelSize(attr, -1));
666                 break;
667 
668             case com.android.internal.R.styleable.TextView_ems:
669                 setEms(a.getInt(attr, -1));
670                 break;
671 
672             case com.android.internal.R.styleable.TextView_width:
673                 setWidth(a.getDimensionPixelSize(attr, -1));
674                 break;
675 
676             case com.android.internal.R.styleable.TextView_minEms:
677                 setMinEms(a.getInt(attr, -1));
678                 break;
679 
680             case com.android.internal.R.styleable.TextView_minWidth:
681                 setMinWidth(a.getDimensionPixelSize(attr, -1));
682                 break;
683 
684             case com.android.internal.R.styleable.TextView_gravity:
685                 setGravity(a.getInt(attr, -1));
686                 break;
687 
688             case com.android.internal.R.styleable.TextView_hint:
689                 hint = a.getText(attr);
690                 break;
691 
692             case com.android.internal.R.styleable.TextView_text:
693                 text = a.getText(attr);
694                 break;
695 
696             case com.android.internal.R.styleable.TextView_scrollHorizontally:
697                 if (a.getBoolean(attr, false)) {
698                     setHorizontallyScrolling(true);
699                 }
700                 break;
701 
702             case com.android.internal.R.styleable.TextView_singleLine:
703                 singleLine = a.getBoolean(attr, singleLine);
704                 break;
705 
706             case com.android.internal.R.styleable.TextView_ellipsize:
707                 ellipsize = a.getInt(attr, ellipsize);
708                 break;
709 
710             case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
711                 setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
712                 break;
713 
714             case com.android.internal.R.styleable.TextView_includeFontPadding:
715                 if (!a.getBoolean(attr, true)) {
716                     setIncludeFontPadding(false);
717                 }
718                 break;
719 
720             case com.android.internal.R.styleable.TextView_cursorVisible:
721                 if (!a.getBoolean(attr, true)) {
722                     setCursorVisible(false);
723                 }
724                 break;
725 
726             case com.android.internal.R.styleable.TextView_maxLength:
727                 maxlength = a.getInt(attr, -1);
728                 break;
729 
730             case com.android.internal.R.styleable.TextView_textScaleX:
731                 setTextScaleX(a.getFloat(attr, 1.0f));
732                 break;
733 
734             case com.android.internal.R.styleable.TextView_freezesText:
735                 mFreezesText = a.getBoolean(attr, false);
736                 break;
737 
738             case com.android.internal.R.styleable.TextView_shadowColor:
739                 shadowcolor = a.getInt(attr, 0);
740                 break;
741 
742             case com.android.internal.R.styleable.TextView_shadowDx:
743                 dx = a.getFloat(attr, 0);
744                 break;
745 
746             case com.android.internal.R.styleable.TextView_shadowDy:
747                 dy = a.getFloat(attr, 0);
748                 break;
749 
750             case com.android.internal.R.styleable.TextView_shadowRadius:
751                 r = a.getFloat(attr, 0);
752                 break;
753 
754             case com.android.internal.R.styleable.TextView_enabled:
755                 setEnabled(a.getBoolean(attr, isEnabled()));
756                 break;
757 
758             case com.android.internal.R.styleable.TextView_textColorHighlight:
759                 textColorHighlight = a.getColor(attr, textColorHighlight);
760                 break;
761 
762             case com.android.internal.R.styleable.TextView_textColor:
763                 textColor = a.getColorStateList(attr);
764                 break;
765 
766             case com.android.internal.R.styleable.TextView_textColorHint:
767                 textColorHint = a.getColorStateList(attr);
768                 break;
769 
770             case com.android.internal.R.styleable.TextView_textColorLink:
771                 textColorLink = a.getColorStateList(attr);
772                 break;
773 
774             case com.android.internal.R.styleable.TextView_textSize:
775                 textSize = a.getDimensionPixelSize(attr, textSize);
776                 break;
777 
778             case com.android.internal.R.styleable.TextView_typeface:
779                 typefaceIndex = a.getInt(attr, typefaceIndex);
780                 break;
781 
782             case com.android.internal.R.styleable.TextView_textStyle:
783                 styleIndex = a.getInt(attr, styleIndex);
784                 break;
785 
786             case com.android.internal.R.styleable.TextView_password:
787                 password = a.getBoolean(attr, password);
788                 break;
789 
790             case com.android.internal.R.styleable.TextView_lineSpacingExtra:
791                 mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
792                 break;
793 
794             case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
795                 mSpacingMult = a.getFloat(attr, mSpacingMult);
796                 break;
797 
798             case com.android.internal.R.styleable.TextView_inputType:
799                 inputType = a.getInt(attr, mInputType);
800                 break;
801 
802             case com.android.internal.R.styleable.TextView_imeOptions:
803                 if (mInputContentType == null) {
804                     mInputContentType = new InputContentType();
805                 }
806                 mInputContentType.imeOptions = a.getInt(attr,
807                         mInputContentType.imeOptions);
808                 break;
809 
810             case com.android.internal.R.styleable.TextView_imeActionLabel:
811                 if (mInputContentType == null) {
812                     mInputContentType = new InputContentType();
813                 }
814                 mInputContentType.imeActionLabel = a.getText(attr);
815                 break;
816 
817             case com.android.internal.R.styleable.TextView_imeActionId:
818                 if (mInputContentType == null) {
819                     mInputContentType = new InputContentType();
820                 }
821                 mInputContentType.imeActionId = a.getInt(attr,
822                         mInputContentType.imeActionId);
823                 break;
824 
825             case com.android.internal.R.styleable.TextView_privateImeOptions:
826                 setPrivateImeOptions(a.getString(attr));
827                 break;
828 
829             case com.android.internal.R.styleable.TextView_editorExtras:
830                 try {
831                     setInputExtras(a.getResourceId(attr, 0));
832                 } catch (XmlPullParserException e) {
833                     Log.w(LOG_TAG, "Failure reading input extras", e);
834                 } catch (IOException e) {
835                     Log.w(LOG_TAG, "Failure reading input extras", e);
836                 }
837                 break;
838 
839             case com.android.internal.R.styleable.TextView_textCursorDrawable:
840                 mCursorDrawableRes = a.getResourceId(attr, 0);
841                 break;
842 
843             case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
844                 mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
845                 break;
846 
847             case com.android.internal.R.styleable.TextView_textSelectHandleRight:
848                 mTextSelectHandleRightRes = a.getResourceId(attr, 0);
849                 break;
850 
851             case com.android.internal.R.styleable.TextView_textSelectHandle:
852                 mTextSelectHandleRes = a.getResourceId(attr, 0);
853                 break;
854 
855             case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
856                 mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
857                 break;
858 
859             case com.android.internal.R.styleable.TextView_textIsSelectable:
860                 mTextIsSelectable = a.getBoolean(attr, false);
861                 break;
862 
863             case com.android.internal.R.styleable.TextView_textAllCaps:
864                 allCaps = a.getBoolean(attr, false);
865                 break;
866             }
867         }
868         a.recycle();
869 
870         BufferType bufferType = BufferType.EDITABLE;
871 
872         final int variation =
873                 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
874         final boolean passwordInputType = variation
875                 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
876         final boolean webPasswordInputType = variation
877                 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
878         final boolean numberPasswordInputType = variation
879                 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
880 
881         if (inputMethod != null) {
882             Class<?> c;
883 
884             try {
885                 c = Class.forName(inputMethod.toString());
886             } catch (ClassNotFoundException ex) {
887                 throw new RuntimeException(ex);
888             }
889 
890             try {
891                 mInput = (KeyListener) c.newInstance();
892             } catch (InstantiationException ex) {
893                 throw new RuntimeException(ex);
894             } catch (IllegalAccessException ex) {
895                 throw new RuntimeException(ex);
896             }
897             try {
898                 mInputType = inputType != EditorInfo.TYPE_NULL
899                         ? inputType
900                         : mInput.getInputType();
901             } catch (IncompatibleClassChangeError e) {
902                 mInputType = EditorInfo.TYPE_CLASS_TEXT;
903             }
904         } else if (digits != null) {
905             mInput = DigitsKeyListener.getInstance(digits.toString());
906             // If no input type was specified, we will default to generic
907             // text, since we can't tell the IME about the set of digits
908             // that was selected.
909             mInputType = inputType != EditorInfo.TYPE_NULL
910                     ? inputType : EditorInfo.TYPE_CLASS_TEXT;
911         } else if (inputType != EditorInfo.TYPE_NULL) {
912             setInputType(inputType, true);
913             // If set, the input type overrides what was set using the deprecated singleLine flag.
914             singleLine = !isMultilineInputType(inputType);
915         } else if (phone) {
916             mInput = DialerKeyListener.getInstance();
917             mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
918         } else if (numeric != 0) {
919             mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
920                                                    (numeric & DECIMAL) != 0);
921             inputType = EditorInfo.TYPE_CLASS_NUMBER;
922             if ((numeric & SIGNED) != 0) {
923                 inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
924             }
925             if ((numeric & DECIMAL) != 0) {
926                 inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
927             }
928             mInputType = inputType;
929         } else if (autotext || autocap != -1) {
930             TextKeyListener.Capitalize cap;
931 
932             inputType = EditorInfo.TYPE_CLASS_TEXT;
933 
934             switch (autocap) {
935             case 1:
936                 cap = TextKeyListener.Capitalize.SENTENCES;
937                 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
938                 break;
939 
940             case 2:
941                 cap = TextKeyListener.Capitalize.WORDS;
942                 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
943                 break;
944 
945             case 3:
946                 cap = TextKeyListener.Capitalize.CHARACTERS;
947                 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
948                 break;
949 
950             default:
951                 cap = TextKeyListener.Capitalize.NONE;
952                 break;
953             }
954 
955             mInput = TextKeyListener.getInstance(autotext, cap);
956             mInputType = inputType;
957         } else if (mTextIsSelectable) {
958             // Prevent text changes from keyboard.
959             mInputType = EditorInfo.TYPE_NULL;
960             mInput = null;
961             bufferType = BufferType.SPANNABLE;
962             // Required to request focus while in touch mode.
963             setFocusableInTouchMode(true);
964             // So that selection can be changed using arrow keys and touch is handled.
965             setMovementMethod(ArrowKeyMovementMethod.getInstance());
966         } else if (editable) {
967             mInput = TextKeyListener.getInstance();
968             mInputType = EditorInfo.TYPE_CLASS_TEXT;
969         } else {
970             mInput = null;
971 
972             switch (buffertype) {
973                 case 0:
974                     bufferType = BufferType.NORMAL;
975                     break;
976                 case 1:
977                     bufferType = BufferType.SPANNABLE;
978                     break;
979                 case 2:
980                     bufferType = BufferType.EDITABLE;
981                     break;
982             }
983         }
984 
985         // mInputType has been set from inputType, possibly modified by mInputMethod.
986         // Specialize mInputType to [web]password if we have a text class and the original input
987         // type was a password.
988         if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
989             if (password || passwordInputType) {
990                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
991                         | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
992             }
993             if (webPasswordInputType) {
994                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
995                         | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
996             }
997         } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
998             if (numberPasswordInputType) {
999                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
1000                         | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
1001             }
1002         }
1003 
1004         if (selectallonfocus) {
1005             mSelectAllOnFocus = true;
1006 
1007             if (bufferType == BufferType.NORMAL)
1008                 bufferType = BufferType.SPANNABLE;
1009         }
1010 
1011         setCompoundDrawablesWithIntrinsicBounds(
1012             drawableLeft, drawableTop, drawableRight, drawableBottom);
1013         setRelativeDrawablesIfNeeded(drawableStart, drawableEnd);
1014         setCompoundDrawablePadding(drawablePadding);
1015 
1016         // Same as setSingleLine(), but make sure the transformation method and the maximum number
1017         // of lines of height are unchanged for multi-line TextViews.
1018         setInputTypeSingleLine(singleLine);
1019         applySingleLine(singleLine, singleLine, singleLine);
1020 
1021         if (singleLine && mInput == null && ellipsize < 0) {
1022                 ellipsize = 3; // END
1023         }
1024 
1025         switch (ellipsize) {
1026             case 1:
1027                 setEllipsize(TextUtils.TruncateAt.START);
1028                 break;
1029             case 2:
1030                 setEllipsize(TextUtils.TruncateAt.MIDDLE);
1031                 break;
1032             case 3:
1033                 setEllipsize(TextUtils.TruncateAt.END);
1034                 break;
1035             case 4:
1036                 if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
1037                     setHorizontalFadingEdgeEnabled(true);
1038                     mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
1039                 } else {
1040                     setHorizontalFadingEdgeEnabled(false);
1041                     mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
1042                 }
1043                 setEllipsize(TextUtils.TruncateAt.MARQUEE);
1044                 break;
1045         }
1046 
1047         setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
1048         setHintTextColor(textColorHint);
1049         setLinkTextColor(textColorLink);
1050         if (textColorHighlight != 0) {
1051             setHighlightColor(textColorHighlight);
1052         }
1053         setRawTextSize(textSize);
1054 
1055         if (allCaps) {
1056             setTransformationMethod(new AllCapsTransformationMethod(getContext()));
1057         }
1058 
1059         if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) {
1060             setTransformationMethod(PasswordTransformationMethod.getInstance());
1061             typefaceIndex = MONOSPACE;
1062         } else if ((mInputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
1063                 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
1064             typefaceIndex = MONOSPACE;
1065         }
1066 
1067         setTypefaceByIndex(typefaceIndex, styleIndex);
1068 
1069         if (shadowcolor != 0) {
1070             setShadowLayer(r, dx, dy, shadowcolor);
1071         }
1072 
1073         if (maxlength >= 0) {
1074             setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
1075         } else {
1076             setFilters(NO_FILTERS);
1077         }
1078 
1079         setText(text, bufferType);
1080         if (hint != null) setHint(hint);
1081 
1082         /*
1083          * Views are not normally focusable unless specified to be.
1084          * However, TextViews that have input or movement methods *are*
1085          * focusable by default.
1086          */
1087         a = context.obtainStyledAttributes(attrs,
1088                                            com.android.internal.R.styleable.View,
1089                                            defStyle, 0);
1090 
1091         boolean focusable = mMovement != null || mInput != null;
1092         boolean clickable = focusable;
1093         boolean longClickable = focusable;
1094 
1095         n = a.getIndexCount();
1096         for (int i = 0; i < n; i++) {
1097             int attr = a.getIndex(i);
1098 
1099             switch (attr) {
1100             case com.android.internal.R.styleable.View_focusable:
1101                 focusable = a.getBoolean(attr, focusable);
1102                 break;
1103 
1104             case com.android.internal.R.styleable.View_clickable:
1105                 clickable = a.getBoolean(attr, clickable);
1106                 break;
1107 
1108             case com.android.internal.R.styleable.View_longClickable:
1109                 longClickable = a.getBoolean(attr, longClickable);
1110                 break;
1111             }
1112         }
1113         a.recycle();
1114 
1115         setFocusable(focusable);
1116         setClickable(clickable);
1117         setLongClickable(longClickable);
1118 
1119         prepareCursorControllers();
1120 
1121         final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
1122         final int touchSlop = viewConfiguration.getScaledTouchSlop();
1123         mSquaredTouchSlopDistance = touchSlop * touchSlop;
1124     }
1125 
setTypefaceByIndex(int typefaceIndex, int styleIndex)1126     private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
1127         Typeface tf = null;
1128         switch (typefaceIndex) {
1129             case SANS:
1130                 tf = Typeface.SANS_SERIF;
1131                 break;
1132 
1133             case SERIF:
1134                 tf = Typeface.SERIF;
1135                 break;
1136 
1137             case MONOSPACE:
1138                 tf = Typeface.MONOSPACE;
1139                 break;
1140         }
1141 
1142         setTypeface(tf, styleIndex);
1143     }
1144 
setRelativeDrawablesIfNeeded(Drawable start, Drawable end)1145     private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) {
1146         boolean hasRelativeDrawables = (start != null) || (end != null);
1147         if (hasRelativeDrawables) {
1148             Drawables dr = mDrawables;
1149             if (dr == null) {
1150                 mDrawables = dr = new Drawables();
1151             }
1152             final Rect compoundRect = dr.mCompoundRect;
1153             int[] state = getDrawableState();
1154             if (start != null) {
1155                 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
1156                 start.setState(state);
1157                 start.copyBounds(compoundRect);
1158                 start.setCallback(this);
1159 
1160                 dr.mDrawableStart = start;
1161                 dr.mDrawableSizeStart = compoundRect.width();
1162                 dr.mDrawableHeightStart = compoundRect.height();
1163             } else {
1164                 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1165             }
1166             if (end != null) {
1167                 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
1168                 end.setState(state);
1169                 end.copyBounds(compoundRect);
1170                 end.setCallback(this);
1171 
1172                 dr.mDrawableEnd = end;
1173                 dr.mDrawableSizeEnd = compoundRect.width();
1174                 dr.mDrawableHeightEnd = compoundRect.height();
1175             } else {
1176                 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1177             }
1178         }
1179     }
1180 
1181     @Override
setEnabled(boolean enabled)1182     public void setEnabled(boolean enabled) {
1183         if (enabled == isEnabled()) {
1184             return;
1185         }
1186 
1187         if (!enabled) {
1188             // Hide the soft input if the currently active TextView is disabled
1189             InputMethodManager imm = InputMethodManager.peekInstance();
1190             if (imm != null && imm.isActive(this)) {
1191                 imm.hideSoftInputFromWindow(getWindowToken(), 0);
1192             }
1193         }
1194         super.setEnabled(enabled);
1195         prepareCursorControllers();
1196         if (enabled) {
1197             // Make sure IME is updated with current editor info.
1198             InputMethodManager imm = InputMethodManager.peekInstance();
1199             if (imm != null) imm.restartInput(this);
1200         }
1201     }
1202 
1203     /**
1204      * Sets the typeface and style in which the text should be displayed,
1205      * and turns on the fake bold and italic bits in the Paint if the
1206      * Typeface that you provided does not have all the bits in the
1207      * style that you specified.
1208      *
1209      * @attr ref android.R.styleable#TextView_typeface
1210      * @attr ref android.R.styleable#TextView_textStyle
1211      */
setTypeface(Typeface tf, int style)1212     public void setTypeface(Typeface tf, int style) {
1213         if (style > 0) {
1214             if (tf == null) {
1215                 tf = Typeface.defaultFromStyle(style);
1216             } else {
1217                 tf = Typeface.create(tf, style);
1218             }
1219 
1220             setTypeface(tf);
1221             // now compute what (if any) algorithmic styling is needed
1222             int typefaceStyle = tf != null ? tf.getStyle() : 0;
1223             int need = style & ~typefaceStyle;
1224             mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
1225             mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
1226         } else {
1227             mTextPaint.setFakeBoldText(false);
1228             mTextPaint.setTextSkewX(0);
1229             setTypeface(tf);
1230         }
1231     }
1232 
1233     /**
1234      * Subclasses override this to specify that they have a KeyListener
1235      * by default even if not specifically called for in the XML options.
1236      */
getDefaultEditable()1237     protected boolean getDefaultEditable() {
1238         return false;
1239     }
1240 
1241     /**
1242      * Subclasses override this to specify a default movement method.
1243      */
getDefaultMovementMethod()1244     protected MovementMethod getDefaultMovementMethod() {
1245         return null;
1246     }
1247 
1248     /**
1249      * Return the text the TextView is displaying. If setText() was called with
1250      * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
1251      * the return value from this method to Spannable or Editable, respectively.
1252      *
1253      * Note: The content of the return value should not be modified. If you want
1254      * a modifiable one, you should make your own copy first.
1255      */
1256     @ViewDebug.CapturedViewProperty
getText()1257     public CharSequence getText() {
1258         return mText;
1259     }
1260 
1261     /**
1262      * Returns the length, in characters, of the text managed by this TextView
1263      */
length()1264     public int length() {
1265         return mText.length();
1266     }
1267 
1268     /**
1269      * Return the text the TextView is displaying as an Editable object.  If
1270      * the text is not editable, null is returned.
1271      *
1272      * @see #getText
1273      */
getEditableText()1274     public Editable getEditableText() {
1275         return (mText instanceof Editable) ? (Editable)mText : null;
1276     }
1277 
1278     /**
1279      * @return the height of one standard line in pixels.  Note that markup
1280      * within the text can cause individual lines to be taller or shorter
1281      * than this height, and the layout may contain additional first-
1282      * or last-line padding.
1283      */
getLineHeight()1284     public int getLineHeight() {
1285         return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
1286     }
1287 
1288     /**
1289      * @return the Layout that is currently being used to display the text.
1290      * This can be null if the text or width has recently changes.
1291      */
getLayout()1292     public final Layout getLayout() {
1293         return mLayout;
1294     }
1295 
1296     /**
1297      * @return the current key listener for this TextView.
1298      * This will frequently be null for non-EditText TextViews.
1299      */
getKeyListener()1300     public final KeyListener getKeyListener() {
1301         return mInput;
1302     }
1303 
1304     /**
1305      * Sets the key listener to be used with this TextView.  This can be null
1306      * to disallow user input.  Note that this method has significant and
1307      * subtle interactions with soft keyboards and other input method:
1308      * see {@link KeyListener#getInputType() KeyListener.getContentType()}
1309      * for important details.  Calling this method will replace the current
1310      * content type of the text view with the content type returned by the
1311      * key listener.
1312      * <p>
1313      * Be warned that if you want a TextView with a key listener or movement
1314      * method not to be focusable, or if you want a TextView without a
1315      * key listener or movement method to be focusable, you must call
1316      * {@link #setFocusable} again after calling this to get the focusability
1317      * back the way you want it.
1318      *
1319      * @attr ref android.R.styleable#TextView_numeric
1320      * @attr ref android.R.styleable#TextView_digits
1321      * @attr ref android.R.styleable#TextView_phoneNumber
1322      * @attr ref android.R.styleable#TextView_inputMethod
1323      * @attr ref android.R.styleable#TextView_capitalize
1324      * @attr ref android.R.styleable#TextView_autoText
1325      */
setKeyListener(KeyListener input)1326     public void setKeyListener(KeyListener input) {
1327         setKeyListenerOnly(input);
1328         fixFocusableAndClickableSettings();
1329 
1330         if (input != null) {
1331             try {
1332                 mInputType = mInput.getInputType();
1333             } catch (IncompatibleClassChangeError e) {
1334                 mInputType = EditorInfo.TYPE_CLASS_TEXT;
1335             }
1336             // Change inputType, without affecting transformation.
1337             // No need to applySingleLine since mSingleLine is unchanged.
1338             setInputTypeSingleLine(mSingleLine);
1339         } else {
1340             mInputType = EditorInfo.TYPE_NULL;
1341         }
1342 
1343         InputMethodManager imm = InputMethodManager.peekInstance();
1344         if (imm != null) imm.restartInput(this);
1345     }
1346 
setKeyListenerOnly(KeyListener input)1347     private void setKeyListenerOnly(KeyListener input) {
1348         mInput = input;
1349         if (mInput != null && !(mText instanceof Editable))
1350             setText(mText);
1351 
1352         setFilters((Editable) mText, mFilters);
1353     }
1354 
1355     /**
1356      * @return the movement method being used for this TextView.
1357      * This will frequently be null for non-EditText TextViews.
1358      */
getMovementMethod()1359     public final MovementMethod getMovementMethod() {
1360         return mMovement;
1361     }
1362 
1363     /**
1364      * Sets the movement method (arrow key handler) to be used for
1365      * this TextView.  This can be null to disallow using the arrow keys
1366      * to move the cursor or scroll the view.
1367      * <p>
1368      * Be warned that if you want a TextView with a key listener or movement
1369      * method not to be focusable, or if you want a TextView without a
1370      * key listener or movement method to be focusable, you must call
1371      * {@link #setFocusable} again after calling this to get the focusability
1372      * back the way you want it.
1373      */
setMovementMethod(MovementMethod movement)1374     public final void setMovementMethod(MovementMethod movement) {
1375         mMovement = movement;
1376 
1377         if (mMovement != null && !(mText instanceof Spannable))
1378             setText(mText);
1379 
1380         fixFocusableAndClickableSettings();
1381 
1382         // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
1383         prepareCursorControllers();
1384     }
1385 
fixFocusableAndClickableSettings()1386     private void fixFocusableAndClickableSettings() {
1387         if ((mMovement != null) || mInput != null) {
1388             setFocusable(true);
1389             setClickable(true);
1390             setLongClickable(true);
1391         } else {
1392             setFocusable(false);
1393             setClickable(false);
1394             setLongClickable(false);
1395         }
1396     }
1397 
1398     /**
1399      * @return the current transformation method for this TextView.
1400      * This will frequently be null except for single-line and password
1401      * fields.
1402      */
getTransformationMethod()1403     public final TransformationMethod getTransformationMethod() {
1404         return mTransformation;
1405     }
1406 
1407     /**
1408      * Sets the transformation that is applied to the text that this
1409      * TextView is displaying.
1410      *
1411      * @attr ref android.R.styleable#TextView_password
1412      * @attr ref android.R.styleable#TextView_singleLine
1413      */
setTransformationMethod(TransformationMethod method)1414     public final void setTransformationMethod(TransformationMethod method) {
1415         if (method == mTransformation) {
1416             // Avoid the setText() below if the transformation is
1417             // the same.
1418             return;
1419         }
1420         if (mTransformation != null) {
1421             if (mText instanceof Spannable) {
1422                 ((Spannable) mText).removeSpan(mTransformation);
1423             }
1424         }
1425 
1426         mTransformation = method;
1427 
1428         if (method instanceof TransformationMethod2) {
1429             TransformationMethod2 method2 = (TransformationMethod2) method;
1430             mAllowTransformationLengthChange = !mTextIsSelectable && !(mText instanceof Editable);
1431             method2.setLengthChangesAllowed(mAllowTransformationLengthChange);
1432         } else {
1433             mAllowTransformationLengthChange = false;
1434         }
1435 
1436         setText(mText);
1437     }
1438 
1439     /**
1440      * Returns the top padding of the view, plus space for the top
1441      * Drawable if any.
1442      */
getCompoundPaddingTop()1443     public int getCompoundPaddingTop() {
1444         final Drawables dr = mDrawables;
1445         if (dr == null || dr.mDrawableTop == null) {
1446             return mPaddingTop;
1447         } else {
1448             return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
1449         }
1450     }
1451 
1452     /**
1453      * Returns the bottom padding of the view, plus space for the bottom
1454      * Drawable if any.
1455      */
getCompoundPaddingBottom()1456     public int getCompoundPaddingBottom() {
1457         final Drawables dr = mDrawables;
1458         if (dr == null || dr.mDrawableBottom == null) {
1459             return mPaddingBottom;
1460         } else {
1461             return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
1462         }
1463     }
1464 
1465     /**
1466      * Returns the left padding of the view, plus space for the left
1467      * Drawable if any.
1468      */
getCompoundPaddingLeft()1469     public int getCompoundPaddingLeft() {
1470         final Drawables dr = mDrawables;
1471         if (dr == null || dr.mDrawableLeft == null) {
1472             return mPaddingLeft;
1473         } else {
1474             return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
1475         }
1476     }
1477 
1478     /**
1479      * Returns the right padding of the view, plus space for the right
1480      * Drawable if any.
1481      */
getCompoundPaddingRight()1482     public int getCompoundPaddingRight() {
1483         final Drawables dr = mDrawables;
1484         if (dr == null || dr.mDrawableRight == null) {
1485             return mPaddingRight;
1486         } else {
1487             return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
1488         }
1489     }
1490 
1491     /**
1492      * Returns the start padding of the view, plus space for the start
1493      * Drawable if any.
1494      *
1495      * @hide
1496      */
getCompoundPaddingStart()1497     public int getCompoundPaddingStart() {
1498         resolveDrawables();
1499         switch(getResolvedLayoutDirection()) {
1500             default:
1501             case LAYOUT_DIRECTION_LTR:
1502                 return getCompoundPaddingLeft();
1503             case LAYOUT_DIRECTION_RTL:
1504                 return getCompoundPaddingRight();
1505         }
1506     }
1507 
1508     /**
1509      * Returns the end padding of the view, plus space for the end
1510      * Drawable if any.
1511      *
1512      * @hide
1513      */
getCompoundPaddingEnd()1514     public int getCompoundPaddingEnd() {
1515         resolveDrawables();
1516         switch(getResolvedLayoutDirection()) {
1517             default:
1518             case LAYOUT_DIRECTION_LTR:
1519                 return getCompoundPaddingRight();
1520             case LAYOUT_DIRECTION_RTL:
1521                 return getCompoundPaddingLeft();
1522         }
1523     }
1524 
1525     /**
1526      * Returns the extended top padding of the view, including both the
1527      * top Drawable if any and any extra space to keep more than maxLines
1528      * of text from showing.  It is only valid to call this after measuring.
1529      */
getExtendedPaddingTop()1530     public int getExtendedPaddingTop() {
1531         if (mMaxMode != LINES) {
1532             return getCompoundPaddingTop();
1533         }
1534 
1535         if (mLayout.getLineCount() <= mMaximum) {
1536             return getCompoundPaddingTop();
1537         }
1538 
1539         int top = getCompoundPaddingTop();
1540         int bottom = getCompoundPaddingBottom();
1541         int viewht = getHeight() - top - bottom;
1542         int layoutht = mLayout.getLineTop(mMaximum);
1543 
1544         if (layoutht >= viewht) {
1545             return top;
1546         }
1547 
1548         final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1549         if (gravity == Gravity.TOP) {
1550             return top;
1551         } else if (gravity == Gravity.BOTTOM) {
1552             return top + viewht - layoutht;
1553         } else { // (gravity == Gravity.CENTER_VERTICAL)
1554             return top + (viewht - layoutht) / 2;
1555         }
1556     }
1557 
1558     /**
1559      * Returns the extended bottom padding of the view, including both the
1560      * bottom Drawable if any and any extra space to keep more than maxLines
1561      * of text from showing.  It is only valid to call this after measuring.
1562      */
getExtendedPaddingBottom()1563     public int getExtendedPaddingBottom() {
1564         if (mMaxMode != LINES) {
1565             return getCompoundPaddingBottom();
1566         }
1567 
1568         if (mLayout.getLineCount() <= mMaximum) {
1569             return getCompoundPaddingBottom();
1570         }
1571 
1572         int top = getCompoundPaddingTop();
1573         int bottom = getCompoundPaddingBottom();
1574         int viewht = getHeight() - top - bottom;
1575         int layoutht = mLayout.getLineTop(mMaximum);
1576 
1577         if (layoutht >= viewht) {
1578             return bottom;
1579         }
1580 
1581         final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1582         if (gravity == Gravity.TOP) {
1583             return bottom + viewht - layoutht;
1584         } else if (gravity == Gravity.BOTTOM) {
1585             return bottom;
1586         } else { // (gravity == Gravity.CENTER_VERTICAL)
1587             return bottom + (viewht - layoutht) / 2;
1588         }
1589     }
1590 
1591     /**
1592      * Returns the total left padding of the view, including the left
1593      * Drawable if any.
1594      */
getTotalPaddingLeft()1595     public int getTotalPaddingLeft() {
1596         return getCompoundPaddingLeft();
1597     }
1598 
1599     /**
1600      * Returns the total right padding of the view, including the right
1601      * Drawable if any.
1602      */
getTotalPaddingRight()1603     public int getTotalPaddingRight() {
1604         return getCompoundPaddingRight();
1605     }
1606 
1607     /**
1608      * Returns the total start padding of the view, including the start
1609      * Drawable if any.
1610      *
1611      * @hide
1612      */
getTotalPaddingStart()1613     public int getTotalPaddingStart() {
1614         return getCompoundPaddingStart();
1615     }
1616 
1617     /**
1618      * Returns the total end padding of the view, including the end
1619      * Drawable if any.
1620      *
1621      * @hide
1622      */
getTotalPaddingEnd()1623     public int getTotalPaddingEnd() {
1624         return getCompoundPaddingEnd();
1625     }
1626 
1627     /**
1628      * Returns the total top padding of the view, including the top
1629      * Drawable if any, the extra space to keep more than maxLines
1630      * from showing, and the vertical offset for gravity, if any.
1631      */
getTotalPaddingTop()1632     public int getTotalPaddingTop() {
1633         return getExtendedPaddingTop() + getVerticalOffset(true);
1634     }
1635 
1636     /**
1637      * Returns the total bottom padding of the view, including the bottom
1638      * Drawable if any, the extra space to keep more than maxLines
1639      * from showing, and the vertical offset for gravity, if any.
1640      */
getTotalPaddingBottom()1641     public int getTotalPaddingBottom() {
1642         return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
1643     }
1644 
1645     /**
1646      * Sets the Drawables (if any) to appear to the left of, above,
1647      * to the right of, and below the text.  Use null if you do not
1648      * want a Drawable there.  The Drawables must already have had
1649      * {@link Drawable#setBounds} called.
1650      *
1651      * @attr ref android.R.styleable#TextView_drawableLeft
1652      * @attr ref android.R.styleable#TextView_drawableTop
1653      * @attr ref android.R.styleable#TextView_drawableRight
1654      * @attr ref android.R.styleable#TextView_drawableBottom
1655      */
setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)1656     public void setCompoundDrawables(Drawable left, Drawable top,
1657                                      Drawable right, Drawable bottom) {
1658         Drawables dr = mDrawables;
1659 
1660         final boolean drawables = left != null || top != null
1661                 || right != null || bottom != null;
1662 
1663         if (!drawables) {
1664             // Clearing drawables...  can we free the data structure?
1665             if (dr != null) {
1666                 if (dr.mDrawablePadding == 0) {
1667                     mDrawables = null;
1668                 } else {
1669                     // We need to retain the last set padding, so just clear
1670                     // out all of the fields in the existing structure.
1671                     if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null);
1672                     dr.mDrawableLeft = null;
1673                     if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
1674                     dr.mDrawableTop = null;
1675                     if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null);
1676                     dr.mDrawableRight = null;
1677                     if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
1678                     dr.mDrawableBottom = null;
1679                     dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1680                     dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1681                     dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1682                     dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1683                 }
1684             }
1685         } else {
1686             if (dr == null) {
1687                 mDrawables = dr = new Drawables();
1688             }
1689 
1690             if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) {
1691                 dr.mDrawableLeft.setCallback(null);
1692             }
1693             dr.mDrawableLeft = left;
1694 
1695             if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
1696                 dr.mDrawableTop.setCallback(null);
1697             }
1698             dr.mDrawableTop = top;
1699 
1700             if (dr.mDrawableRight != right && dr.mDrawableRight != null) {
1701                 dr.mDrawableRight.setCallback(null);
1702             }
1703             dr.mDrawableRight = right;
1704 
1705             if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
1706                 dr.mDrawableBottom.setCallback(null);
1707             }
1708             dr.mDrawableBottom = bottom;
1709 
1710             final Rect compoundRect = dr.mCompoundRect;
1711             int[] state;
1712 
1713             state = getDrawableState();
1714 
1715             if (left != null) {
1716                 left.setState(state);
1717                 left.copyBounds(compoundRect);
1718                 left.setCallback(this);
1719                 dr.mDrawableSizeLeft = compoundRect.width();
1720                 dr.mDrawableHeightLeft = compoundRect.height();
1721             } else {
1722                 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1723             }
1724 
1725             if (right != null) {
1726                 right.setState(state);
1727                 right.copyBounds(compoundRect);
1728                 right.setCallback(this);
1729                 dr.mDrawableSizeRight = compoundRect.width();
1730                 dr.mDrawableHeightRight = compoundRect.height();
1731             } else {
1732                 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1733             }
1734 
1735             if (top != null) {
1736                 top.setState(state);
1737                 top.copyBounds(compoundRect);
1738                 top.setCallback(this);
1739                 dr.mDrawableSizeTop = compoundRect.height();
1740                 dr.mDrawableWidthTop = compoundRect.width();
1741             } else {
1742                 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1743             }
1744 
1745             if (bottom != null) {
1746                 bottom.setState(state);
1747                 bottom.copyBounds(compoundRect);
1748                 bottom.setCallback(this);
1749                 dr.mDrawableSizeBottom = compoundRect.height();
1750                 dr.mDrawableWidthBottom = compoundRect.width();
1751             } else {
1752                 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1753             }
1754         }
1755 
1756         invalidate();
1757         requestLayout();
1758     }
1759 
1760     /**
1761      * Sets the Drawables (if any) to appear to the left of, above,
1762      * to the right of, and below the text.  Use 0 if you do not
1763      * want a Drawable there. The Drawables' bounds will be set to
1764      * their intrinsic bounds.
1765      *
1766      * @param left Resource identifier of the left Drawable.
1767      * @param top Resource identifier of the top Drawable.
1768      * @param right Resource identifier of the right Drawable.
1769      * @param bottom Resource identifier of the bottom Drawable.
1770      *
1771      * @attr ref android.R.styleable#TextView_drawableLeft
1772      * @attr ref android.R.styleable#TextView_drawableTop
1773      * @attr ref android.R.styleable#TextView_drawableRight
1774      * @attr ref android.R.styleable#TextView_drawableBottom
1775      */
setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom)1776     public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
1777         final Resources resources = getContext().getResources();
1778         setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null,
1779                 top != 0 ? resources.getDrawable(top) : null,
1780                 right != 0 ? resources.getDrawable(right) : null,
1781                 bottom != 0 ? resources.getDrawable(bottom) : null);
1782     }
1783 
1784     /**
1785      * Sets the Drawables (if any) to appear to the left of, above,
1786      * to the right of, and below the text.  Use null if you do not
1787      * want a Drawable there. The Drawables' bounds will be set to
1788      * their intrinsic bounds.
1789      *
1790      * @attr ref android.R.styleable#TextView_drawableLeft
1791      * @attr ref android.R.styleable#TextView_drawableTop
1792      * @attr ref android.R.styleable#TextView_drawableRight
1793      * @attr ref android.R.styleable#TextView_drawableBottom
1794      */
setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom)1795     public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top,
1796             Drawable right, Drawable bottom) {
1797 
1798         if (left != null) {
1799             left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
1800         }
1801         if (right != null) {
1802             right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
1803         }
1804         if (top != null) {
1805             top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1806         }
1807         if (bottom != null) {
1808             bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1809         }
1810         setCompoundDrawables(left, top, right, bottom);
1811     }
1812 
1813     /**
1814      * Sets the Drawables (if any) to appear to the start of, above,
1815      * to the end of, and below the text.  Use null if you do not
1816      * want a Drawable there.  The Drawables must already have had
1817      * {@link Drawable#setBounds} called.
1818      *
1819      * @attr ref android.R.styleable#TextView_drawableStart
1820      * @attr ref android.R.styleable#TextView_drawableTop
1821      * @attr ref android.R.styleable#TextView_drawableEnd
1822      * @attr ref android.R.styleable#TextView_drawableBottom
1823      *
1824      * @hide
1825      */
setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, Drawable bottom)1826     public void setCompoundDrawablesRelative(Drawable start, Drawable top,
1827                                      Drawable end, Drawable bottom) {
1828         Drawables dr = mDrawables;
1829 
1830         final boolean drawables = start != null || top != null
1831                 || end != null || bottom != null;
1832 
1833         if (!drawables) {
1834             // Clearing drawables...  can we free the data structure?
1835             if (dr != null) {
1836                 if (dr.mDrawablePadding == 0) {
1837                     mDrawables = null;
1838                 } else {
1839                     // We need to retain the last set padding, so just clear
1840                     // out all of the fields in the existing structure.
1841                     if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
1842                     dr.mDrawableStart = null;
1843                     if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
1844                     dr.mDrawableTop = null;
1845                     if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null);
1846                     dr.mDrawableEnd = null;
1847                     if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
1848                     dr.mDrawableBottom = null;
1849                     dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1850                     dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1851                     dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1852                     dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1853                 }
1854             }
1855         } else {
1856             if (dr == null) {
1857                 mDrawables = dr = new Drawables();
1858             }
1859 
1860             if (dr.mDrawableStart != start && dr.mDrawableStart != null) {
1861                 dr.mDrawableStart.setCallback(null);
1862             }
1863             dr.mDrawableStart = start;
1864 
1865             if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
1866                 dr.mDrawableTop.setCallback(null);
1867             }
1868             dr.mDrawableTop = top;
1869 
1870             if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) {
1871                 dr.mDrawableEnd.setCallback(null);
1872             }
1873             dr.mDrawableEnd = end;
1874 
1875             if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
1876                 dr.mDrawableBottom.setCallback(null);
1877             }
1878             dr.mDrawableBottom = bottom;
1879 
1880             final Rect compoundRect = dr.mCompoundRect;
1881             int[] state;
1882 
1883             state = getDrawableState();
1884 
1885             if (start != null) {
1886                 start.setState(state);
1887                 start.copyBounds(compoundRect);
1888                 start.setCallback(this);
1889                 dr.mDrawableSizeStart = compoundRect.width();
1890                 dr.mDrawableHeightStart = compoundRect.height();
1891             } else {
1892                 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1893             }
1894 
1895             if (end != null) {
1896                 end.setState(state);
1897                 end.copyBounds(compoundRect);
1898                 end.setCallback(this);
1899                 dr.mDrawableSizeEnd = compoundRect.width();
1900                 dr.mDrawableHeightEnd = compoundRect.height();
1901             } else {
1902                 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1903             }
1904 
1905             if (top != null) {
1906                 top.setState(state);
1907                 top.copyBounds(compoundRect);
1908                 top.setCallback(this);
1909                 dr.mDrawableSizeTop = compoundRect.height();
1910                 dr.mDrawableWidthTop = compoundRect.width();
1911             } else {
1912                 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1913             }
1914 
1915             if (bottom != null) {
1916                 bottom.setState(state);
1917                 bottom.copyBounds(compoundRect);
1918                 bottom.setCallback(this);
1919                 dr.mDrawableSizeBottom = compoundRect.height();
1920                 dr.mDrawableWidthBottom = compoundRect.width();
1921             } else {
1922                 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1923             }
1924         }
1925 
1926         resolveDrawables();
1927         invalidate();
1928         requestLayout();
1929     }
1930 
1931     /**
1932      * Sets the Drawables (if any) to appear to the start of, above,
1933      * to the end of, and below the text.  Use 0 if you do not
1934      * want a Drawable there. The Drawables' bounds will be set to
1935      * their intrinsic bounds.
1936      *
1937      * @param start Resource identifier of the start Drawable.
1938      * @param top Resource identifier of the top Drawable.
1939      * @param end Resource identifier of the end Drawable.
1940      * @param bottom Resource identifier of the bottom Drawable.
1941      *
1942      * @attr ref android.R.styleable#TextView_drawableStart
1943      * @attr ref android.R.styleable#TextView_drawableTop
1944      * @attr ref android.R.styleable#TextView_drawableEnd
1945      * @attr ref android.R.styleable#TextView_drawableBottom
1946      *
1947      * @hide
1948      */
setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end, int bottom)1949     public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end,
1950             int bottom) {
1951         resetResolvedDrawables();
1952         final Resources resources = getContext().getResources();
1953         setCompoundDrawablesRelativeWithIntrinsicBounds(
1954                 start != 0 ? resources.getDrawable(start) : null,
1955                 top != 0 ? resources.getDrawable(top) : null,
1956                 end != 0 ? resources.getDrawable(end) : null,
1957                 bottom != 0 ? resources.getDrawable(bottom) : null);
1958     }
1959 
1960     /**
1961      * Sets the Drawables (if any) to appear to the start of, above,
1962      * to the end of, and below the text.  Use null if you do not
1963      * want a Drawable there. The Drawables' bounds will be set to
1964      * their intrinsic bounds.
1965      *
1966      * @attr ref android.R.styleable#TextView_drawableStart
1967      * @attr ref android.R.styleable#TextView_drawableTop
1968      * @attr ref android.R.styleable#TextView_drawableEnd
1969      * @attr ref android.R.styleable#TextView_drawableBottom
1970      *
1971      * @hide
1972      */
setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable start, Drawable top, Drawable end, Drawable bottom)1973     public void setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable start, Drawable top,
1974             Drawable end, Drawable bottom) {
1975 
1976         resetResolvedDrawables();
1977         if (start != null) {
1978             start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
1979         }
1980         if (end != null) {
1981             end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
1982         }
1983         if (top != null) {
1984             top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1985         }
1986         if (bottom != null) {
1987             bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1988         }
1989         setCompoundDrawablesRelative(start, top, end, bottom);
1990     }
1991 
1992     /**
1993      * Returns drawables for the left, top, right, and bottom borders.
1994      */
getCompoundDrawables()1995     public Drawable[] getCompoundDrawables() {
1996         final Drawables dr = mDrawables;
1997         if (dr != null) {
1998             return new Drawable[] {
1999                 dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom
2000             };
2001         } else {
2002             return new Drawable[] { null, null, null, null };
2003         }
2004     }
2005 
2006     /**
2007      * Returns drawables for the start, top, end, and bottom borders.
2008      *
2009      * @hide
2010      */
getCompoundDrawablesRelative()2011     public Drawable[] getCompoundDrawablesRelative() {
2012         final Drawables dr = mDrawables;
2013         if (dr != null) {
2014             return new Drawable[] {
2015                 dr.mDrawableStart, dr.mDrawableTop, dr.mDrawableEnd, dr.mDrawableBottom
2016             };
2017         } else {
2018             return new Drawable[] { null, null, null, null };
2019         }
2020     }
2021 
2022     /**
2023      * Sets the size of the padding between the compound drawables and
2024      * the text.
2025      *
2026      * @attr ref android.R.styleable#TextView_drawablePadding
2027      */
setCompoundDrawablePadding(int pad)2028     public void setCompoundDrawablePadding(int pad) {
2029         Drawables dr = mDrawables;
2030         if (pad == 0) {
2031             if (dr != null) {
2032                 dr.mDrawablePadding = pad;
2033             }
2034         } else {
2035             if (dr == null) {
2036                 mDrawables = dr = new Drawables();
2037             }
2038             dr.mDrawablePadding = pad;
2039         }
2040 
2041         invalidate();
2042         requestLayout();
2043     }
2044 
2045     /**
2046      * Returns the padding between the compound drawables and the text.
2047      */
getCompoundDrawablePadding()2048     public int getCompoundDrawablePadding() {
2049         final Drawables dr = mDrawables;
2050         return dr != null ? dr.mDrawablePadding : 0;
2051     }
2052 
2053     @Override
setPadding(int left, int top, int right, int bottom)2054     public void setPadding(int left, int top, int right, int bottom) {
2055         if (left != mPaddingLeft ||
2056             right != mPaddingRight ||
2057             top != mPaddingTop ||
2058             bottom != mPaddingBottom) {
2059             nullLayouts();
2060         }
2061 
2062         // the super call will requestLayout()
2063         super.setPadding(left, top, right, bottom);
2064         invalidate();
2065     }
2066 
2067     /**
2068      * Gets the autolink mask of the text.  See {@link
2069      * android.text.util.Linkify#ALL Linkify.ALL} and peers for
2070      * possible values.
2071      *
2072      * @attr ref android.R.styleable#TextView_autoLink
2073      */
getAutoLinkMask()2074     public final int getAutoLinkMask() {
2075         return mAutoLinkMask;
2076     }
2077 
2078     /**
2079      * Sets the text color, size, style, hint color, and highlight color
2080      * from the specified TextAppearance resource.
2081      */
setTextAppearance(Context context, int resid)2082     public void setTextAppearance(Context context, int resid) {
2083         TypedArray appearance =
2084             context.obtainStyledAttributes(resid,
2085                                            com.android.internal.R.styleable.TextAppearance);
2086 
2087         int color;
2088         ColorStateList colors;
2089         int ts;
2090 
2091         color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
2092         if (color != 0) {
2093             setHighlightColor(color);
2094         }
2095 
2096         colors = appearance.getColorStateList(com.android.internal.R.styleable.
2097                                               TextAppearance_textColor);
2098         if (colors != null) {
2099             setTextColor(colors);
2100         }
2101 
2102         ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
2103                                               TextAppearance_textSize, 0);
2104         if (ts != 0) {
2105             setRawTextSize(ts);
2106         }
2107 
2108         colors = appearance.getColorStateList(com.android.internal.R.styleable.
2109                                               TextAppearance_textColorHint);
2110         if (colors != null) {
2111             setHintTextColor(colors);
2112         }
2113 
2114         colors = appearance.getColorStateList(com.android.internal.R.styleable.
2115                                               TextAppearance_textColorLink);
2116         if (colors != null) {
2117             setLinkTextColor(colors);
2118         }
2119 
2120         int typefaceIndex, styleIndex;
2121 
2122         typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
2123                                           TextAppearance_typeface, -1);
2124         styleIndex = appearance.getInt(com.android.internal.R.styleable.
2125                                        TextAppearance_textStyle, -1);
2126 
2127         setTypefaceByIndex(typefaceIndex, styleIndex);
2128 
2129         if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps,
2130                 false)) {
2131             setTransformationMethod(new AllCapsTransformationMethod(getContext()));
2132         }
2133 
2134         appearance.recycle();
2135     }
2136 
2137     /**
2138      * @return the size (in pixels) of the default text size in this TextView.
2139      */
getTextSize()2140     public float getTextSize() {
2141         return mTextPaint.getTextSize();
2142     }
2143 
2144     /**
2145      * Set the default text size to the given value, interpreted as "scaled
2146      * pixel" units.  This size is adjusted based on the current density and
2147      * user font size preference.
2148      *
2149      * @param size The scaled pixel size.
2150      *
2151      * @attr ref android.R.styleable#TextView_textSize
2152      */
2153     @android.view.RemotableViewMethod
setTextSize(float size)2154     public void setTextSize(float size) {
2155         setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
2156     }
2157 
2158     /**
2159      * Set the default text size to a given unit and value.  See {@link
2160      * TypedValue} for the possible dimension units.
2161      *
2162      * @param unit The desired dimension unit.
2163      * @param size The desired size in the given units.
2164      *
2165      * @attr ref android.R.styleable#TextView_textSize
2166      */
setTextSize(int unit, float size)2167     public void setTextSize(int unit, float size) {
2168         Context c = getContext();
2169         Resources r;
2170 
2171         if (c == null)
2172             r = Resources.getSystem();
2173         else
2174             r = c.getResources();
2175 
2176         setRawTextSize(TypedValue.applyDimension(
2177             unit, size, r.getDisplayMetrics()));
2178     }
2179 
setRawTextSize(float size)2180     private void setRawTextSize(float size) {
2181         if (size != mTextPaint.getTextSize()) {
2182             mTextPaint.setTextSize(size);
2183 
2184             if (mLayout != null) {
2185                 nullLayouts();
2186                 requestLayout();
2187                 invalidate();
2188             }
2189         }
2190     }
2191 
2192     /**
2193      * @return the extent by which text is currently being stretched
2194      * horizontally.  This will usually be 1.
2195      */
getTextScaleX()2196     public float getTextScaleX() {
2197         return mTextPaint.getTextScaleX();
2198     }
2199 
2200     /**
2201      * Sets the extent by which text should be stretched horizontally.
2202      *
2203      * @attr ref android.R.styleable#TextView_textScaleX
2204      */
2205     @android.view.RemotableViewMethod
setTextScaleX(float size)2206     public void setTextScaleX(float size) {
2207         if (size != mTextPaint.getTextScaleX()) {
2208             mUserSetTextScaleX = true;
2209             mTextPaint.setTextScaleX(size);
2210 
2211             if (mLayout != null) {
2212                 nullLayouts();
2213                 requestLayout();
2214                 invalidate();
2215             }
2216         }
2217     }
2218 
2219     /**
2220      * Sets the typeface and style in which the text should be displayed.
2221      * Note that not all Typeface families actually have bold and italic
2222      * variants, so you may need to use
2223      * {@link #setTypeface(Typeface, int)} to get the appearance
2224      * that you actually want.
2225      *
2226      * @attr ref android.R.styleable#TextView_typeface
2227      * @attr ref android.R.styleable#TextView_textStyle
2228      */
setTypeface(Typeface tf)2229     public void setTypeface(Typeface tf) {
2230         if (mTextPaint.getTypeface() != tf) {
2231             mTextPaint.setTypeface(tf);
2232 
2233             if (mLayout != null) {
2234                 nullLayouts();
2235                 requestLayout();
2236                 invalidate();
2237             }
2238         }
2239     }
2240 
2241     /**
2242      * @return the current typeface and style in which the text is being
2243      * displayed.
2244      */
getTypeface()2245     public Typeface getTypeface() {
2246         return mTextPaint.getTypeface();
2247     }
2248 
2249     /**
2250      * Sets the text color for all the states (normal, selected,
2251      * focused) to be this color.
2252      *
2253      * @attr ref android.R.styleable#TextView_textColor
2254      */
2255     @android.view.RemotableViewMethod
setTextColor(int color)2256     public void setTextColor(int color) {
2257         mTextColor = ColorStateList.valueOf(color);
2258         updateTextColors();
2259     }
2260 
2261     /**
2262      * Sets the text color.
2263      *
2264      * @attr ref android.R.styleable#TextView_textColor
2265      */
setTextColor(ColorStateList colors)2266     public void setTextColor(ColorStateList colors) {
2267         if (colors == null) {
2268             throw new NullPointerException();
2269         }
2270 
2271         mTextColor = colors;
2272         updateTextColors();
2273     }
2274 
2275     /**
2276      * Return the set of text colors.
2277      *
2278      * @return Returns the set of text colors.
2279      */
getTextColors()2280     public final ColorStateList getTextColors() {
2281         return mTextColor;
2282     }
2283 
2284     /**
2285      * <p>Return the current color selected for normal text.</p>
2286      *
2287      * @return Returns the current text color.
2288      */
getCurrentTextColor()2289     public final int getCurrentTextColor() {
2290         return mCurTextColor;
2291     }
2292 
2293     /**
2294      * Sets the color used to display the selection highlight.
2295      *
2296      * @attr ref android.R.styleable#TextView_textColorHighlight
2297      */
2298     @android.view.RemotableViewMethod
setHighlightColor(int color)2299     public void setHighlightColor(int color) {
2300         if (mHighlightColor != color) {
2301             mHighlightColor = color;
2302             invalidate();
2303         }
2304     }
2305 
2306     /**
2307      * Gives the text a shadow of the specified radius and color, the specified
2308      * distance from its normal position.
2309      *
2310      * @attr ref android.R.styleable#TextView_shadowColor
2311      * @attr ref android.R.styleable#TextView_shadowDx
2312      * @attr ref android.R.styleable#TextView_shadowDy
2313      * @attr ref android.R.styleable#TextView_shadowRadius
2314      */
setShadowLayer(float radius, float dx, float dy, int color)2315     public void setShadowLayer(float radius, float dx, float dy, int color) {
2316         mTextPaint.setShadowLayer(radius, dx, dy, color);
2317 
2318         mShadowRadius = radius;
2319         mShadowDx = dx;
2320         mShadowDy = dy;
2321 
2322         invalidate();
2323     }
2324 
2325     /**
2326      * @return the base paint used for the text.  Please use this only to
2327      * consult the Paint's properties and not to change them.
2328      */
getPaint()2329     public TextPaint getPaint() {
2330         return mTextPaint;
2331     }
2332 
2333     /**
2334      * Sets the autolink mask of the text.  See {@link
2335      * android.text.util.Linkify#ALL Linkify.ALL} and peers for
2336      * possible values.
2337      *
2338      * @attr ref android.R.styleable#TextView_autoLink
2339      */
2340     @android.view.RemotableViewMethod
setAutoLinkMask(int mask)2341     public final void setAutoLinkMask(int mask) {
2342         mAutoLinkMask = mask;
2343     }
2344 
2345     /**
2346      * Sets whether the movement method will automatically be set to
2347      * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
2348      * set to nonzero and links are detected in {@link #setText}.
2349      * The default is true.
2350      *
2351      * @attr ref android.R.styleable#TextView_linksClickable
2352      */
2353     @android.view.RemotableViewMethod
setLinksClickable(boolean whether)2354     public final void setLinksClickable(boolean whether) {
2355         mLinksClickable = whether;
2356     }
2357 
2358     /**
2359      * Returns whether the movement method will automatically be set to
2360      * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
2361      * set to nonzero and links are detected in {@link #setText}.
2362      * The default is true.
2363      *
2364      * @attr ref android.R.styleable#TextView_linksClickable
2365      */
getLinksClickable()2366     public final boolean getLinksClickable() {
2367         return mLinksClickable;
2368     }
2369 
2370     /**
2371      * Returns the list of URLSpans attached to the text
2372      * (by {@link Linkify} or otherwise) if any.  You can call
2373      * {@link URLSpan#getURL} on them to find where they link to
2374      * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
2375      * to find the region of the text they are attached to.
2376      */
getUrls()2377     public URLSpan[] getUrls() {
2378         if (mText instanceof Spanned) {
2379             return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
2380         } else {
2381             return new URLSpan[0];
2382         }
2383     }
2384 
2385     /**
2386      * Sets the color of the hint text.
2387      *
2388      * @attr ref android.R.styleable#TextView_textColorHint
2389      */
2390     @android.view.RemotableViewMethod
setHintTextColor(int color)2391     public final void setHintTextColor(int color) {
2392         mHintTextColor = ColorStateList.valueOf(color);
2393         updateTextColors();
2394     }
2395 
2396     /**
2397      * Sets the color of the hint text.
2398      *
2399      * @attr ref android.R.styleable#TextView_textColorHint
2400      */
setHintTextColor(ColorStateList colors)2401     public final void setHintTextColor(ColorStateList colors) {
2402         mHintTextColor = colors;
2403         updateTextColors();
2404     }
2405 
2406     /**
2407      * <p>Return the color used to paint the hint text.</p>
2408      *
2409      * @return Returns the list of hint text colors.
2410      */
getHintTextColors()2411     public final ColorStateList getHintTextColors() {
2412         return mHintTextColor;
2413     }
2414 
2415     /**
2416      * <p>Return the current color selected to paint the hint text.</p>
2417      *
2418      * @return Returns the current hint text color.
2419      */
getCurrentHintTextColor()2420     public final int getCurrentHintTextColor() {
2421         return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
2422     }
2423 
2424     /**
2425      * Sets the color of links in the text.
2426      *
2427      * @attr ref android.R.styleable#TextView_textColorLink
2428      */
2429     @android.view.RemotableViewMethod
setLinkTextColor(int color)2430     public final void setLinkTextColor(int color) {
2431         mLinkTextColor = ColorStateList.valueOf(color);
2432         updateTextColors();
2433     }
2434 
2435     /**
2436      * Sets the color of links in the text.
2437      *
2438      * @attr ref android.R.styleable#TextView_textColorLink
2439      */
setLinkTextColor(ColorStateList colors)2440     public final void setLinkTextColor(ColorStateList colors) {
2441         mLinkTextColor = colors;
2442         updateTextColors();
2443     }
2444 
2445     /**
2446      * <p>Returns the color used to paint links in the text.</p>
2447      *
2448      * @return Returns the list of link text colors.
2449      */
getLinkTextColors()2450     public final ColorStateList getLinkTextColors() {
2451         return mLinkTextColor;
2452     }
2453 
2454     /**
2455      * Sets the horizontal alignment of the text and the
2456      * vertical gravity that will be used when there is extra space
2457      * in the TextView beyond what is required for the text itself.
2458      *
2459      * @see android.view.Gravity
2460      * @attr ref android.R.styleable#TextView_gravity
2461      */
setGravity(int gravity)2462     public void setGravity(int gravity) {
2463         if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
2464             gravity |= Gravity.START;
2465         }
2466         if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
2467             gravity |= Gravity.TOP;
2468         }
2469 
2470         boolean newLayout = false;
2471 
2472         if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) !=
2473             (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) {
2474             newLayout = true;
2475         }
2476 
2477         if (gravity != mGravity) {
2478             invalidate();
2479             mLayoutAlignment = null;
2480         }
2481 
2482         mGravity = gravity;
2483 
2484         if (mLayout != null && newLayout) {
2485             // XXX this is heavy-handed because no actual content changes.
2486             int want = mLayout.getWidth();
2487             int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
2488 
2489             makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
2490                           mRight - mLeft - getCompoundPaddingLeft() -
2491                           getCompoundPaddingRight(), true);
2492         }
2493     }
2494 
2495     /**
2496      * Returns the horizontal and vertical alignment of this TextView.
2497      *
2498      * @see android.view.Gravity
2499      * @attr ref android.R.styleable#TextView_gravity
2500      */
getGravity()2501     public int getGravity() {
2502         return mGravity;
2503     }
2504 
2505     /**
2506      * @return the flags on the Paint being used to display the text.
2507      * @see Paint#getFlags
2508      */
getPaintFlags()2509     public int getPaintFlags() {
2510         return mTextPaint.getFlags();
2511     }
2512 
2513     /**
2514      * Sets flags on the Paint being used to display the text and
2515      * reflows the text if they are different from the old flags.
2516      * @see Paint#setFlags
2517      */
2518     @android.view.RemotableViewMethod
setPaintFlags(int flags)2519     public void setPaintFlags(int flags) {
2520         if (mTextPaint.getFlags() != flags) {
2521             mTextPaint.setFlags(flags);
2522 
2523             if (mLayout != null) {
2524                 nullLayouts();
2525                 requestLayout();
2526                 invalidate();
2527             }
2528         }
2529     }
2530 
2531     /**
2532      * Sets whether the text should be allowed to be wider than the
2533      * View is.  If false, it will be wrapped to the width of the View.
2534      *
2535      * @attr ref android.R.styleable#TextView_scrollHorizontally
2536      */
setHorizontallyScrolling(boolean whether)2537     public void setHorizontallyScrolling(boolean whether) {
2538         if (mHorizontallyScrolling != whether) {
2539             mHorizontallyScrolling = whether;
2540 
2541             if (mLayout != null) {
2542                 nullLayouts();
2543                 requestLayout();
2544                 invalidate();
2545             }
2546         }
2547     }
2548 
2549     /**
2550      * Makes the TextView at least this many lines tall.
2551      *
2552      * Setting this value overrides any other (minimum) height setting. A single line TextView will
2553      * set this value to 1.
2554      *
2555      * @attr ref android.R.styleable#TextView_minLines
2556      */
2557     @android.view.RemotableViewMethod
setMinLines(int minlines)2558     public void setMinLines(int minlines) {
2559         mMinimum = minlines;
2560         mMinMode = LINES;
2561 
2562         requestLayout();
2563         invalidate();
2564     }
2565 
2566     /**
2567      * Makes the TextView at least this many pixels tall.
2568      *
2569      * Setting this value overrides any other (minimum) number of lines setting.
2570      *
2571      * @attr ref android.R.styleable#TextView_minHeight
2572      */
2573     @android.view.RemotableViewMethod
setMinHeight(int minHeight)2574     public void setMinHeight(int minHeight) {
2575         mMinimum = minHeight;
2576         mMinMode = PIXELS;
2577 
2578         requestLayout();
2579         invalidate();
2580     }
2581 
2582     /**
2583      * Makes the TextView at most this many lines tall.
2584      *
2585      * Setting this value overrides any other (maximum) height setting.
2586      *
2587      * @attr ref android.R.styleable#TextView_maxLines
2588      */
2589     @android.view.RemotableViewMethod
setMaxLines(int maxlines)2590     public void setMaxLines(int maxlines) {
2591         mMaximum = maxlines;
2592         mMaxMode = LINES;
2593 
2594         requestLayout();
2595         invalidate();
2596     }
2597 
2598     /**
2599      * Makes the TextView at most this many pixels tall.  This option is mutually exclusive with the
2600      * {@link #setMaxLines(int)} method.
2601      *
2602      * Setting this value overrides any other (maximum) number of lines setting.
2603      *
2604      * @attr ref android.R.styleable#TextView_maxHeight
2605      */
2606     @android.view.RemotableViewMethod
setMaxHeight(int maxHeight)2607     public void setMaxHeight(int maxHeight) {
2608         mMaximum = maxHeight;
2609         mMaxMode = PIXELS;
2610 
2611         requestLayout();
2612         invalidate();
2613     }
2614 
2615     /**
2616      * Makes the TextView exactly this many lines tall.
2617      *
2618      * Note that setting this value overrides any other (minimum / maximum) number of lines or
2619      * height setting. A single line TextView will set this value to 1.
2620      *
2621      * @attr ref android.R.styleable#TextView_lines
2622      */
2623     @android.view.RemotableViewMethod
setLines(int lines)2624     public void setLines(int lines) {
2625         mMaximum = mMinimum = lines;
2626         mMaxMode = mMinMode = LINES;
2627 
2628         requestLayout();
2629         invalidate();
2630     }
2631 
2632     /**
2633      * Makes the TextView exactly this many pixels tall.
2634      * You could do the same thing by specifying this number in the
2635      * LayoutParams.
2636      *
2637      * Note that setting this value overrides any other (minimum / maximum) number of lines or
2638      * height setting.
2639      *
2640      * @attr ref android.R.styleable#TextView_height
2641      */
2642     @android.view.RemotableViewMethod
setHeight(int pixels)2643     public void setHeight(int pixels) {
2644         mMaximum = mMinimum = pixels;
2645         mMaxMode = mMinMode = PIXELS;
2646 
2647         requestLayout();
2648         invalidate();
2649     }
2650 
2651     /**
2652      * Makes the TextView at least this many ems wide
2653      *
2654      * @attr ref android.R.styleable#TextView_minEms
2655      */
2656     @android.view.RemotableViewMethod
setMinEms(int minems)2657     public void setMinEms(int minems) {
2658         mMinWidth = minems;
2659         mMinWidthMode = EMS;
2660 
2661         requestLayout();
2662         invalidate();
2663     }
2664 
2665     /**
2666      * Makes the TextView at least this many pixels wide
2667      *
2668      * @attr ref android.R.styleable#TextView_minWidth
2669      */
2670     @android.view.RemotableViewMethod
setMinWidth(int minpixels)2671     public void setMinWidth(int minpixels) {
2672         mMinWidth = minpixels;
2673         mMinWidthMode = PIXELS;
2674 
2675         requestLayout();
2676         invalidate();
2677     }
2678 
2679     /**
2680      * Makes the TextView at most this many ems wide
2681      *
2682      * @attr ref android.R.styleable#TextView_maxEms
2683      */
2684     @android.view.RemotableViewMethod
setMaxEms(int maxems)2685     public void setMaxEms(int maxems) {
2686         mMaxWidth = maxems;
2687         mMaxWidthMode = EMS;
2688 
2689         requestLayout();
2690         invalidate();
2691     }
2692 
2693     /**
2694      * Makes the TextView at most this many pixels wide
2695      *
2696      * @attr ref android.R.styleable#TextView_maxWidth
2697      */
2698     @android.view.RemotableViewMethod
setMaxWidth(int maxpixels)2699     public void setMaxWidth(int maxpixels) {
2700         mMaxWidth = maxpixels;
2701         mMaxWidthMode = PIXELS;
2702 
2703         requestLayout();
2704         invalidate();
2705     }
2706 
2707     /**
2708      * Makes the TextView exactly this many ems wide
2709      *
2710      * @attr ref android.R.styleable#TextView_ems
2711      */
2712     @android.view.RemotableViewMethod
setEms(int ems)2713     public void setEms(int ems) {
2714         mMaxWidth = mMinWidth = ems;
2715         mMaxWidthMode = mMinWidthMode = EMS;
2716 
2717         requestLayout();
2718         invalidate();
2719     }
2720 
2721     /**
2722      * Makes the TextView exactly this many pixels wide.
2723      * You could do the same thing by specifying this number in the
2724      * LayoutParams.
2725      *
2726      * @attr ref android.R.styleable#TextView_width
2727      */
2728     @android.view.RemotableViewMethod
setWidth(int pixels)2729     public void setWidth(int pixels) {
2730         mMaxWidth = mMinWidth = pixels;
2731         mMaxWidthMode = mMinWidthMode = PIXELS;
2732 
2733         requestLayout();
2734         invalidate();
2735     }
2736 
2737 
2738     /**
2739      * Sets line spacing for this TextView.  Each line will have its height
2740      * multiplied by <code>mult</code> and have <code>add</code> added to it.
2741      *
2742      * @attr ref android.R.styleable#TextView_lineSpacingExtra
2743      * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
2744      */
setLineSpacing(float add, float mult)2745     public void setLineSpacing(float add, float mult) {
2746         if (mSpacingAdd != add || mSpacingMult != mult) {
2747             mSpacingAdd = add;
2748             mSpacingMult = mult;
2749 
2750             if (mLayout != null) {
2751                 nullLayouts();
2752                 requestLayout();
2753                 invalidate();
2754             }
2755         }
2756     }
2757 
2758     /**
2759      * Convenience method: Append the specified text to the TextView's
2760      * display buffer, upgrading it to BufferType.EDITABLE if it was
2761      * not already editable.
2762      */
append(CharSequence text)2763     public final void append(CharSequence text) {
2764         append(text, 0, text.length());
2765     }
2766 
2767     /**
2768      * Convenience method: Append the specified text slice to the TextView's
2769      * display buffer, upgrading it to BufferType.EDITABLE if it was
2770      * not already editable.
2771      */
append(CharSequence text, int start, int end)2772     public void append(CharSequence text, int start, int end) {
2773         if (!(mText instanceof Editable)) {
2774             setText(mText, BufferType.EDITABLE);
2775         }
2776 
2777         ((Editable) mText).append(text, start, end);
2778     }
2779 
updateTextColors()2780     private void updateTextColors() {
2781         boolean inval = false;
2782         int color = mTextColor.getColorForState(getDrawableState(), 0);
2783         if (color != mCurTextColor) {
2784             mCurTextColor = color;
2785             inval = true;
2786         }
2787         if (mLinkTextColor != null) {
2788             color = mLinkTextColor.getColorForState(getDrawableState(), 0);
2789             if (color != mTextPaint.linkColor) {
2790                 mTextPaint.linkColor = color;
2791                 inval = true;
2792             }
2793         }
2794         if (mHintTextColor != null) {
2795             color = mHintTextColor.getColorForState(getDrawableState(), 0);
2796             if (color != mCurHintTextColor && mText.length() == 0) {
2797                 mCurHintTextColor = color;
2798                 inval = true;
2799             }
2800         }
2801         if (inval) {
2802             invalidate();
2803         }
2804     }
2805 
2806     @Override
drawableStateChanged()2807     protected void drawableStateChanged() {
2808         super.drawableStateChanged();
2809         if (mTextColor != null && mTextColor.isStateful()
2810                 || (mHintTextColor != null && mHintTextColor.isStateful())
2811                 || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
2812             updateTextColors();
2813         }
2814 
2815         final Drawables dr = mDrawables;
2816         if (dr != null) {
2817             int[] state = getDrawableState();
2818             if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
2819                 dr.mDrawableTop.setState(state);
2820             }
2821             if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
2822                 dr.mDrawableBottom.setState(state);
2823             }
2824             if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
2825                 dr.mDrawableLeft.setState(state);
2826             }
2827             if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
2828                 dr.mDrawableRight.setState(state);
2829             }
2830             if (dr.mDrawableStart != null && dr.mDrawableStart.isStateful()) {
2831                 dr.mDrawableStart.setState(state);
2832             }
2833             if (dr.mDrawableEnd != null && dr.mDrawableEnd.isStateful()) {
2834                 dr.mDrawableEnd.setState(state);
2835             }
2836         }
2837     }
2838 
2839     /**
2840      * User interface state that is stored by TextView for implementing
2841      * {@link View#onSaveInstanceState}.
2842      */
2843     public static class SavedState extends BaseSavedState {
2844         int selStart;
2845         int selEnd;
2846         CharSequence text;
2847         boolean frozenWithFocus;
2848         CharSequence error;
2849 
SavedState(Parcelable superState)2850         SavedState(Parcelable superState) {
2851             super(superState);
2852         }
2853 
2854         @Override
writeToParcel(Parcel out, int flags)2855         public void writeToParcel(Parcel out, int flags) {
2856             super.writeToParcel(out, flags);
2857             out.writeInt(selStart);
2858             out.writeInt(selEnd);
2859             out.writeInt(frozenWithFocus ? 1 : 0);
2860             TextUtils.writeToParcel(text, out, flags);
2861 
2862             if (error == null) {
2863                 out.writeInt(0);
2864             } else {
2865                 out.writeInt(1);
2866                 TextUtils.writeToParcel(error, out, flags);
2867             }
2868         }
2869 
2870         @Override
toString()2871         public String toString() {
2872             String str = "TextView.SavedState{"
2873                     + Integer.toHexString(System.identityHashCode(this))
2874                     + " start=" + selStart + " end=" + selEnd;
2875             if (text != null) {
2876                 str += " text=" + text;
2877             }
2878             return str + "}";
2879         }
2880 
2881         @SuppressWarnings("hiding")
2882         public static final Parcelable.Creator<SavedState> CREATOR
2883                 = new Parcelable.Creator<SavedState>() {
2884             public SavedState createFromParcel(Parcel in) {
2885                 return new SavedState(in);
2886             }
2887 
2888             public SavedState[] newArray(int size) {
2889                 return new SavedState[size];
2890             }
2891         };
2892 
SavedState(Parcel in)2893         private SavedState(Parcel in) {
2894             super(in);
2895             selStart = in.readInt();
2896             selEnd = in.readInt();
2897             frozenWithFocus = (in.readInt() != 0);
2898             text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2899 
2900             if (in.readInt() != 0) {
2901                 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2902             }
2903         }
2904     }
2905 
2906     @Override
onSaveInstanceState()2907     public Parcelable onSaveInstanceState() {
2908         Parcelable superState = super.onSaveInstanceState();
2909 
2910         // Save state if we are forced to
2911         boolean save = mFreezesText;
2912         int start = 0;
2913         int end = 0;
2914 
2915         if (mText != null) {
2916             start = getSelectionStart();
2917             end = getSelectionEnd();
2918             if (start >= 0 || end >= 0) {
2919                 // Or save state if there is a selection
2920                 save = true;
2921             }
2922         }
2923 
2924         if (save) {
2925             SavedState ss = new SavedState(superState);
2926             // XXX Should also save the current scroll position!
2927             ss.selStart = start;
2928             ss.selEnd = end;
2929 
2930             if (mText instanceof Spanned) {
2931                 /*
2932                  * Calling setText() strips off any ChangeWatchers;
2933                  * strip them now to avoid leaking references.
2934                  * But do it to a copy so that if there are any
2935                  * further changes to the text of this view, it
2936                  * won't get into an inconsistent state.
2937                  */
2938 
2939                 Spannable sp = new SpannableString(mText);
2940 
2941                 for (ChangeWatcher cw : sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
2942                     sp.removeSpan(cw);
2943                 }
2944 
2945                 SuggestionSpan[] suggestionSpans = sp.getSpans(0, sp.length(), SuggestionSpan.class);
2946                 for (int i = 0; i < suggestionSpans.length; i++) {
2947                     int flags = suggestionSpans[i].getFlags();
2948                     if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
2949                             && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2950                         sp.removeSpan(suggestionSpans[i]);
2951                     }
2952                 }
2953 
2954                 sp.removeSpan(mSuggestionRangeSpan);
2955 
2956                 ss.text = sp;
2957             } else {
2958                 ss.text = mText.toString();
2959             }
2960 
2961             if (isFocused() && start >= 0 && end >= 0) {
2962                 ss.frozenWithFocus = true;
2963             }
2964 
2965             ss.error = mError;
2966 
2967             return ss;
2968         }
2969 
2970         return superState;
2971     }
2972 
2973     @Override
onRestoreInstanceState(Parcelable state)2974     public void onRestoreInstanceState(Parcelable state) {
2975         if (!(state instanceof SavedState)) {
2976             super.onRestoreInstanceState(state);
2977             return;
2978         }
2979 
2980         SavedState ss = (SavedState)state;
2981         super.onRestoreInstanceState(ss.getSuperState());
2982 
2983         // XXX restore buffer type too, as well as lots of other stuff
2984         if (ss.text != null) {
2985             setText(ss.text);
2986         }
2987 
2988         if (ss.selStart >= 0 && ss.selEnd >= 0) {
2989             if (mText instanceof Spannable) {
2990                 int len = mText.length();
2991 
2992                 if (ss.selStart > len || ss.selEnd > len) {
2993                     String restored = "";
2994 
2995                     if (ss.text != null) {
2996                         restored = "(restored) ";
2997                     }
2998 
2999                     Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
3000                           "/" + ss.selEnd + " out of range for " + restored +
3001                           "text " + mText);
3002                 } else {
3003                     Selection.setSelection((Spannable) mText, ss.selStart,
3004                                            ss.selEnd);
3005 
3006                     if (ss.frozenWithFocus) {
3007                         mFrozenWithFocus = true;
3008                     }
3009                 }
3010             }
3011         }
3012 
3013         if (ss.error != null) {
3014             final CharSequence error = ss.error;
3015             // Display the error later, after the first layout pass
3016             post(new Runnable() {
3017                 public void run() {
3018                     setError(error);
3019                 }
3020             });
3021         }
3022     }
3023 
3024     /**
3025      * Control whether this text view saves its entire text contents when
3026      * freezing to an icicle, in addition to dynamic state such as cursor
3027      * position.  By default this is false, not saving the text.  Set to true
3028      * if the text in the text view is not being saved somewhere else in
3029      * persistent storage (such as in a content provider) so that if the
3030      * view is later thawed the user will not lose their data.
3031      *
3032      * @param freezesText Controls whether a frozen icicle should include the
3033      * entire text data: true to include it, false to not.
3034      *
3035      * @attr ref android.R.styleable#TextView_freezesText
3036      */
3037     @android.view.RemotableViewMethod
setFreezesText(boolean freezesText)3038     public void setFreezesText(boolean freezesText) {
3039         mFreezesText = freezesText;
3040     }
3041 
3042     /**
3043      * Return whether this text view is including its entire text contents
3044      * in frozen icicles.
3045      *
3046      * @return Returns true if text is included, false if it isn't.
3047      *
3048      * @see #setFreezesText
3049      */
getFreezesText()3050     public boolean getFreezesText() {
3051         return mFreezesText;
3052     }
3053 
3054     ///////////////////////////////////////////////////////////////////////////
3055 
3056     /**
3057      * Sets the Factory used to create new Editables.
3058      */
setEditableFactory(Editable.Factory factory)3059     public final void setEditableFactory(Editable.Factory factory) {
3060         mEditableFactory = factory;
3061         setText(mText);
3062     }
3063 
3064     /**
3065      * Sets the Factory used to create new Spannables.
3066      */
setSpannableFactory(Spannable.Factory factory)3067     public final void setSpannableFactory(Spannable.Factory factory) {
3068         mSpannableFactory = factory;
3069         setText(mText);
3070     }
3071 
3072     /**
3073      * Sets the string value of the TextView. TextView <em>does not</em> accept
3074      * HTML-like formatting, which you can do with text strings in XML resource files.
3075      * To style your strings, attach android.text.style.* objects to a
3076      * {@link android.text.SpannableString SpannableString}, or see the
3077      * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
3078      * Available Resource Types</a> documentation for an example of setting
3079      * formatted text in the XML resource file.
3080      *
3081      * @attr ref android.R.styleable#TextView_text
3082      */
3083     @android.view.RemotableViewMethod
setText(CharSequence text)3084     public final void setText(CharSequence text) {
3085         setText(text, mBufferType);
3086     }
3087 
3088     /**
3089      * Like {@link #setText(CharSequence)},
3090      * except that the cursor position (if any) is retained in the new text.
3091      *
3092      * @param text The new text to place in the text view.
3093      *
3094      * @see #setText(CharSequence)
3095      */
3096     @android.view.RemotableViewMethod
setTextKeepState(CharSequence text)3097     public final void setTextKeepState(CharSequence text) {
3098         setTextKeepState(text, mBufferType);
3099     }
3100 
3101     /**
3102      * Sets the text that this TextView is to display (see
3103      * {@link #setText(CharSequence)}) and also sets whether it is stored
3104      * in a styleable/spannable buffer and whether it is editable.
3105      *
3106      * @attr ref android.R.styleable#TextView_text
3107      * @attr ref android.R.styleable#TextView_bufferType
3108      */
setText(CharSequence text, BufferType type)3109     public void setText(CharSequence text, BufferType type) {
3110         setText(text, type, true, 0);
3111 
3112         if (mCharWrapper != null) {
3113             mCharWrapper.mChars = null;
3114         }
3115     }
3116 
setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen)3117     private void setText(CharSequence text, BufferType type,
3118                          boolean notifyBefore, int oldlen) {
3119         if (text == null) {
3120             text = "";
3121         }
3122 
3123         // If suggestions are not enabled, remove the suggestion spans from the text
3124         if (!isSuggestionsEnabled()) {
3125             text = removeSuggestionSpans(text);
3126         }
3127 
3128         if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
3129 
3130         if (text instanceof Spanned &&
3131             ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
3132             if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
3133                 setHorizontalFadingEdgeEnabled(true);
3134                 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
3135             } else {
3136                 setHorizontalFadingEdgeEnabled(false);
3137                 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
3138             }
3139             setEllipsize(TextUtils.TruncateAt.MARQUEE);
3140         }
3141 
3142         int n = mFilters.length;
3143         for (int i = 0; i < n; i++) {
3144             CharSequence out = mFilters[i].filter(text, 0, text.length(),
3145                                                   EMPTY_SPANNED, 0, 0);
3146             if (out != null) {
3147                 text = out;
3148             }
3149         }
3150 
3151         if (notifyBefore) {
3152             if (mText != null) {
3153                 oldlen = mText.length();
3154                 sendBeforeTextChanged(mText, 0, oldlen, text.length());
3155             } else {
3156                 sendBeforeTextChanged("", 0, 0, text.length());
3157             }
3158         }
3159 
3160         boolean needEditableForNotification = false;
3161         boolean startSpellCheck = false;
3162 
3163         if (mListeners != null && mListeners.size() != 0) {
3164             needEditableForNotification = true;
3165         }
3166 
3167         if (type == BufferType.EDITABLE || mInput != null || needEditableForNotification) {
3168             Editable t = mEditableFactory.newEditable(text);
3169             text = t;
3170             setFilters(t, mFilters);
3171             InputMethodManager imm = InputMethodManager.peekInstance();
3172             if (imm != null) imm.restartInput(this);
3173             startSpellCheck = true;
3174         } else if (type == BufferType.SPANNABLE || mMovement != null) {
3175             text = mSpannableFactory.newSpannable(text);
3176         } else if (!(text instanceof CharWrapper)) {
3177             text = TextUtils.stringOrSpannedString(text);
3178         }
3179 
3180         if (mAutoLinkMask != 0) {
3181             Spannable s2;
3182 
3183             if (type == BufferType.EDITABLE || text instanceof Spannable) {
3184                 s2 = (Spannable) text;
3185             } else {
3186                 s2 = mSpannableFactory.newSpannable(text);
3187             }
3188 
3189             if (Linkify.addLinks(s2, mAutoLinkMask)) {
3190                 text = s2;
3191                 type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
3192 
3193                 /*
3194                  * We must go ahead and set the text before changing the
3195                  * movement method, because setMovementMethod() may call
3196                  * setText() again to try to upgrade the buffer type.
3197                  */
3198                 mText = text;
3199 
3200                 // Do not change the movement method for text that support text selection as it
3201                 // would prevent an arbitrary cursor displacement.
3202                 if (mLinksClickable && !textCanBeSelected()) {
3203                     setMovementMethod(LinkMovementMethod.getInstance());
3204                 }
3205             }
3206         }
3207 
3208         mBufferType = type;
3209         mText = text;
3210 
3211         if (mTransformation == null) {
3212             mTransformed = text;
3213         } else {
3214             mTransformed = mTransformation.getTransformation(text, this);
3215         }
3216 
3217         final int textLength = text.length();
3218 
3219         if (text instanceof Spannable && !mAllowTransformationLengthChange) {
3220             Spannable sp = (Spannable) text;
3221 
3222             // Remove any ChangeWatchers that might have come
3223             // from other TextViews.
3224             final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
3225             final int count = watchers.length;
3226             for (int i = 0; i < count; i++)
3227                 sp.removeSpan(watchers[i]);
3228 
3229             if (mChangeWatcher == null)
3230                 mChangeWatcher = new ChangeWatcher();
3231 
3232             sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
3233                        (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
3234 
3235             if (mInput != null) {
3236                 sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
3237             }
3238 
3239             if (mTransformation != null) {
3240                 sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
3241             }
3242 
3243             if (mMovement != null) {
3244                 mMovement.initialize(this, (Spannable) text);
3245 
3246                 /*
3247                  * Initializing the movement method will have set the
3248                  * selection, so reset mSelectionMoved to keep that from
3249                  * interfering with the normal on-focus selection-setting.
3250                  */
3251                 mSelectionMoved = false;
3252             }
3253         }
3254 
3255         if (mLayout != null) {
3256             checkForRelayout();
3257         }
3258 
3259         sendOnTextChanged(text, 0, oldlen, textLength);
3260         onTextChanged(text, 0, oldlen, textLength);
3261 
3262         if (startSpellCheck && mSpellChecker != null) {
3263             // This view has to have been previously attached for mSpellChecker to exist
3264             updateSpellCheckSpans(0, textLength);
3265         }
3266 
3267         if (needEditableForNotification) {
3268             sendAfterTextChanged((Editable) text);
3269         }
3270 
3271         // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
3272         prepareCursorControllers();
3273     }
3274 
3275     /**
3276      * Sets the TextView to display the specified slice of the specified
3277      * char array.  You must promise that you will not change the contents
3278      * of the array except for right before another call to setText(),
3279      * since the TextView has no way to know that the text
3280      * has changed and that it needs to invalidate and re-layout.
3281      */
setText(char[] text, int start, int len)3282     public final void setText(char[] text, int start, int len) {
3283         int oldlen = 0;
3284 
3285         if (start < 0 || len < 0 || start + len > text.length) {
3286             throw new IndexOutOfBoundsException(start + ", " + len);
3287         }
3288 
3289         /*
3290          * We must do the before-notification here ourselves because if
3291          * the old text is a CharWrapper we destroy it before calling
3292          * into the normal path.
3293          */
3294         if (mText != null) {
3295             oldlen = mText.length();
3296             sendBeforeTextChanged(mText, 0, oldlen, len);
3297         } else {
3298             sendBeforeTextChanged("", 0, 0, len);
3299         }
3300 
3301         if (mCharWrapper == null) {
3302             mCharWrapper = new CharWrapper(text, start, len);
3303         } else {
3304             mCharWrapper.set(text, start, len);
3305         }
3306 
3307         setText(mCharWrapper, mBufferType, false, oldlen);
3308     }
3309 
3310     private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations {
3311         private char[] mChars;
3312         private int mStart, mLength;
3313 
CharWrapper(char[] chars, int start, int len)3314         public CharWrapper(char[] chars, int start, int len) {
3315             mChars = chars;
3316             mStart = start;
3317             mLength = len;
3318         }
3319 
set(char[] chars, int start, int len)3320         /* package */ void set(char[] chars, int start, int len) {
3321             mChars = chars;
3322             mStart = start;
3323             mLength = len;
3324         }
3325 
length()3326         public int length() {
3327             return mLength;
3328         }
3329 
charAt(int off)3330         public char charAt(int off) {
3331             return mChars[off + mStart];
3332         }
3333 
3334         @Override
toString()3335         public String toString() {
3336             return new String(mChars, mStart, mLength);
3337         }
3338 
subSequence(int start, int end)3339         public CharSequence subSequence(int start, int end) {
3340             if (start < 0 || end < 0 || start > mLength || end > mLength) {
3341                 throw new IndexOutOfBoundsException(start + ", " + end);
3342             }
3343 
3344             return new String(mChars, start + mStart, end - start);
3345         }
3346 
getChars(int start, int end, char[] buf, int off)3347         public void getChars(int start, int end, char[] buf, int off) {
3348             if (start < 0 || end < 0 || start > mLength || end > mLength) {
3349                 throw new IndexOutOfBoundsException(start + ", " + end);
3350             }
3351 
3352             System.arraycopy(mChars, start + mStart, buf, off, end - start);
3353         }
3354 
drawText(Canvas c, int start, int end, float x, float y, Paint p)3355         public void drawText(Canvas c, int start, int end,
3356                              float x, float y, Paint p) {
3357             c.drawText(mChars, start + mStart, end - start, x, y, p);
3358         }
3359 
drawTextRun(Canvas c, int start, int end, int contextStart, int contextEnd, float x, float y, int flags, Paint p)3360         public void drawTextRun(Canvas c, int start, int end,
3361                 int contextStart, int contextEnd, float x, float y, int flags, Paint p) {
3362             int count = end - start;
3363             int contextCount = contextEnd - contextStart;
3364             c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
3365                     contextCount, x, y, flags, p);
3366         }
3367 
measureText(int start, int end, Paint p)3368         public float measureText(int start, int end, Paint p) {
3369             return p.measureText(mChars, start + mStart, end - start);
3370         }
3371 
getTextWidths(int start, int end, float[] widths, Paint p)3372         public int getTextWidths(int start, int end, float[] widths, Paint p) {
3373             return p.getTextWidths(mChars, start + mStart, end - start, widths);
3374         }
3375 
getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags, float[] advances, int advancesIndex, Paint p)3376         public float getTextRunAdvances(int start, int end, int contextStart,
3377                 int contextEnd, int flags, float[] advances, int advancesIndex,
3378                 Paint p) {
3379             int count = end - start;
3380             int contextCount = contextEnd - contextStart;
3381             return p.getTextRunAdvances(mChars, start + mStart, count,
3382                     contextStart + mStart, contextCount, flags, advances,
3383                     advancesIndex);
3384         }
3385 
getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags, float[] advances, int advancesIndex, Paint p, int reserved)3386         public float getTextRunAdvances(int start, int end, int contextStart,
3387                 int contextEnd, int flags, float[] advances, int advancesIndex,
3388                 Paint p, int reserved) {
3389             int count = end - start;
3390             int contextCount = contextEnd - contextStart;
3391             return p.getTextRunAdvances(mChars, start + mStart, count,
3392                     contextStart + mStart, contextCount, flags, advances,
3393                     advancesIndex, reserved);
3394         }
3395 
getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, int cursorOpt, Paint p)3396         public int getTextRunCursor(int contextStart, int contextEnd, int flags,
3397                 int offset, int cursorOpt, Paint p) {
3398             int contextCount = contextEnd - contextStart;
3399             return p.getTextRunCursor(mChars, contextStart + mStart,
3400                     contextCount, flags, offset + mStart, cursorOpt);
3401         }
3402     }
3403 
3404     /**
3405      * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
3406      * except that the cursor position (if any) is retained in the new text.
3407      *
3408      * @see #setText(CharSequence, android.widget.TextView.BufferType)
3409      */
setTextKeepState(CharSequence text, BufferType type)3410     public final void setTextKeepState(CharSequence text, BufferType type) {
3411         int start = getSelectionStart();
3412         int end = getSelectionEnd();
3413         int len = text.length();
3414 
3415         setText(text, type);
3416 
3417         if (start >= 0 || end >= 0) {
3418             if (mText instanceof Spannable) {
3419                 Selection.setSelection((Spannable) mText,
3420                                        Math.max(0, Math.min(start, len)),
3421                                        Math.max(0, Math.min(end, len)));
3422             }
3423         }
3424     }
3425 
3426     @android.view.RemotableViewMethod
setText(int resid)3427     public final void setText(int resid) {
3428         setText(getContext().getResources().getText(resid));
3429     }
3430 
setText(int resid, BufferType type)3431     public final void setText(int resid, BufferType type) {
3432         setText(getContext().getResources().getText(resid), type);
3433     }
3434 
3435     /**
3436      * Sets the text to be displayed when the text of the TextView is empty.
3437      * Null means to use the normal empty text. The hint does not currently
3438      * participate in determining the size of the view.
3439      *
3440      * @attr ref android.R.styleable#TextView_hint
3441      */
3442     @android.view.RemotableViewMethod
setHint(CharSequence hint)3443     public final void setHint(CharSequence hint) {
3444         mHint = TextUtils.stringOrSpannedString(hint);
3445 
3446         if (mLayout != null) {
3447             checkForRelayout();
3448         }
3449 
3450         if (mText.length() == 0) {
3451             invalidate();
3452         }
3453     }
3454 
3455     /**
3456      * Sets the text to be displayed when the text of the TextView is empty,
3457      * from a resource.
3458      *
3459      * @attr ref android.R.styleable#TextView_hint
3460      */
3461     @android.view.RemotableViewMethod
setHint(int resid)3462     public final void setHint(int resid) {
3463         setHint(getContext().getResources().getText(resid));
3464     }
3465 
3466     /**
3467      * Returns the hint that is displayed when the text of the TextView
3468      * is empty.
3469      *
3470      * @attr ref android.R.styleable#TextView_hint
3471      */
3472     @ViewDebug.CapturedViewProperty
getHint()3473     public CharSequence getHint() {
3474         return mHint;
3475     }
3476 
isMultilineInputType(int type)3477     private static boolean isMultilineInputType(int type) {
3478         return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
3479             (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
3480     }
3481 
3482     /**
3483      * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
3484      * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
3485      * to match the given content type.  If the given content type is {@link EditorInfo#TYPE_NULL}
3486      * then a soft keyboard will not be displayed for this text view.
3487      *
3488      * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
3489      * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
3490      * type.
3491      *
3492      * @see #getInputType()
3493      * @see #setRawInputType(int)
3494      * @see android.text.InputType
3495      * @attr ref android.R.styleable#TextView_inputType
3496      */
setInputType(int type)3497     public void setInputType(int type) {
3498         final boolean wasPassword = isPasswordInputType(mInputType);
3499         final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType);
3500         setInputType(type, false);
3501         final boolean isPassword = isPasswordInputType(type);
3502         final boolean isVisiblePassword = isVisiblePasswordInputType(type);
3503         boolean forceUpdate = false;
3504         if (isPassword) {
3505             setTransformationMethod(PasswordTransformationMethod.getInstance());
3506             setTypefaceByIndex(MONOSPACE, 0);
3507         } else if (isVisiblePassword) {
3508             if (mTransformation == PasswordTransformationMethod.getInstance()) {
3509                 forceUpdate = true;
3510             }
3511             setTypefaceByIndex(MONOSPACE, 0);
3512         } else if (wasPassword || wasVisiblePassword) {
3513             // not in password mode, clean up typeface and transformation
3514             setTypefaceByIndex(-1, -1);
3515             if (mTransformation == PasswordTransformationMethod.getInstance()) {
3516                 forceUpdate = true;
3517             }
3518         }
3519 
3520         boolean singleLine = !isMultilineInputType(type);
3521 
3522         // We need to update the single line mode if it has changed or we
3523         // were previously in password mode.
3524         if (mSingleLine != singleLine || forceUpdate) {
3525             // Change single line mode, but only change the transformation if
3526             // we are not in password mode.
3527             applySingleLine(singleLine, !isPassword, true);
3528         }
3529 
3530         if (!isSuggestionsEnabled()) {
3531             mText = removeSuggestionSpans(mText);
3532         }
3533 
3534         InputMethodManager imm = InputMethodManager.peekInstance();
3535         if (imm != null) imm.restartInput(this);
3536     }
3537 
3538     /**
3539      * It would be better to rely on the input type for everything. A password inputType should have
3540      * a password transformation. We should hence use isPasswordInputType instead of this method.
3541      *
3542      * We should:
3543      * - Call setInputType in setKeyListener instead of changing the input type directly (which
3544      * would install the correct transformation).
3545      * - Refuse the installation of a non-password transformation in setTransformation if the input
3546      * type is password.
3547      *
3548      * However, this is like this for legacy reasons and we cannot break existing apps. This method
3549      * is useful since it matches what the user can see (obfuscated text or not).
3550      *
3551      * @return true if the current transformation method is of the password type.
3552      */
hasPasswordTransformationMethod()3553     private boolean hasPasswordTransformationMethod() {
3554         return mTransformation instanceof PasswordTransformationMethod;
3555     }
3556 
isPasswordInputType(int inputType)3557     private static boolean isPasswordInputType(int inputType) {
3558         final int variation =
3559                 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
3560         return variation
3561                 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
3562                 || variation
3563                 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
3564                 || variation
3565                 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
3566     }
3567 
isVisiblePasswordInputType(int inputType)3568     private static boolean isVisiblePasswordInputType(int inputType) {
3569         final int variation =
3570                 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
3571         return variation
3572                 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
3573     }
3574 
3575     /**
3576      * Directly change the content type integer of the text view, without
3577      * modifying any other state.
3578      * @see #setInputType(int)
3579      * @see android.text.InputType
3580      * @attr ref android.R.styleable#TextView_inputType
3581      */
setRawInputType(int type)3582     public void setRawInputType(int type) {
3583         mInputType = type;
3584     }
3585 
setInputType(int type, boolean direct)3586     private void setInputType(int type, boolean direct) {
3587         final int cls = type & EditorInfo.TYPE_MASK_CLASS;
3588         KeyListener input;
3589         if (cls == EditorInfo.TYPE_CLASS_TEXT) {
3590             boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
3591             TextKeyListener.Capitalize cap;
3592             if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
3593                 cap = TextKeyListener.Capitalize.CHARACTERS;
3594             } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
3595                 cap = TextKeyListener.Capitalize.WORDS;
3596             } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
3597                 cap = TextKeyListener.Capitalize.SENTENCES;
3598             } else {
3599                 cap = TextKeyListener.Capitalize.NONE;
3600             }
3601             input = TextKeyListener.getInstance(autotext, cap);
3602         } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
3603             input = DigitsKeyListener.getInstance(
3604                     (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
3605                     (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
3606         } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
3607             switch (type & EditorInfo.TYPE_MASK_VARIATION) {
3608                 case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
3609                     input = DateKeyListener.getInstance();
3610                     break;
3611                 case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
3612                     input = TimeKeyListener.getInstance();
3613                     break;
3614                 default:
3615                     input = DateTimeKeyListener.getInstance();
3616                     break;
3617             }
3618         } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
3619             input = DialerKeyListener.getInstance();
3620         } else {
3621             input = TextKeyListener.getInstance();
3622         }
3623         setRawInputType(type);
3624         if (direct) mInput = input;
3625         else {
3626             setKeyListenerOnly(input);
3627         }
3628     }
3629 
3630     /**
3631      * Get the type of the content.
3632      *
3633      * @see #setInputType(int)
3634      * @see android.text.InputType
3635      */
getInputType()3636     public int getInputType() {
3637         return mInputType;
3638     }
3639 
3640     /**
3641      * Change the editor type integer associated with the text view, which
3642      * will be reported to an IME with {@link EditorInfo#imeOptions} when it
3643      * has focus.
3644      * @see #getImeOptions
3645      * @see android.view.inputmethod.EditorInfo
3646      * @attr ref android.R.styleable#TextView_imeOptions
3647      */
setImeOptions(int imeOptions)3648     public void setImeOptions(int imeOptions) {
3649         if (mInputContentType == null) {
3650             mInputContentType = new InputContentType();
3651         }
3652         mInputContentType.imeOptions = imeOptions;
3653     }
3654 
3655     /**
3656      * Get the type of the IME editor.
3657      *
3658      * @see #setImeOptions(int)
3659      * @see android.view.inputmethod.EditorInfo
3660      */
getImeOptions()3661     public int getImeOptions() {
3662         return mInputContentType != null
3663                 ? mInputContentType.imeOptions : EditorInfo.IME_NULL;
3664     }
3665 
3666     /**
3667      * Change the custom IME action associated with the text view, which
3668      * will be reported to an IME with {@link EditorInfo#actionLabel}
3669      * and {@link EditorInfo#actionId} when it has focus.
3670      * @see #getImeActionLabel
3671      * @see #getImeActionId
3672      * @see android.view.inputmethod.EditorInfo
3673      * @attr ref android.R.styleable#TextView_imeActionLabel
3674      * @attr ref android.R.styleable#TextView_imeActionId
3675      */
setImeActionLabel(CharSequence label, int actionId)3676     public void setImeActionLabel(CharSequence label, int actionId) {
3677         if (mInputContentType == null) {
3678             mInputContentType = new InputContentType();
3679         }
3680         mInputContentType.imeActionLabel = label;
3681         mInputContentType.imeActionId = actionId;
3682     }
3683 
3684     /**
3685      * Get the IME action label previous set with {@link #setImeActionLabel}.
3686      *
3687      * @see #setImeActionLabel
3688      * @see android.view.inputmethod.EditorInfo
3689      */
getImeActionLabel()3690     public CharSequence getImeActionLabel() {
3691         return mInputContentType != null
3692                 ? mInputContentType.imeActionLabel : null;
3693     }
3694 
3695     /**
3696      * Get the IME action ID previous set with {@link #setImeActionLabel}.
3697      *
3698      * @see #setImeActionLabel
3699      * @see android.view.inputmethod.EditorInfo
3700      */
getImeActionId()3701     public int getImeActionId() {
3702         return mInputContentType != null
3703                 ? mInputContentType.imeActionId : 0;
3704     }
3705 
3706     /**
3707      * Set a special listener to be called when an action is performed
3708      * on the text view.  This will be called when the enter key is pressed,
3709      * or when an action supplied to the IME is selected by the user.  Setting
3710      * this means that the normal hard key event will not insert a newline
3711      * into the text view, even if it is multi-line; holding down the ALT
3712      * modifier will, however, allow the user to insert a newline character.
3713      */
setOnEditorActionListener(OnEditorActionListener l)3714     public void setOnEditorActionListener(OnEditorActionListener l) {
3715         if (mInputContentType == null) {
3716             mInputContentType = new InputContentType();
3717         }
3718         mInputContentType.onEditorActionListener = l;
3719     }
3720 
3721     /**
3722      * Called when an attached input method calls
3723      * {@link InputConnection#performEditorAction(int)
3724      * InputConnection.performEditorAction()}
3725      * for this text view.  The default implementation will call your action
3726      * listener supplied to {@link #setOnEditorActionListener}, or perform
3727      * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
3728      * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
3729      * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
3730      * EditorInfo.IME_ACTION_DONE}.
3731      *
3732      * <p>For backwards compatibility, if no IME options have been set and the
3733      * text view would not normally advance focus on enter, then
3734      * the NEXT and DONE actions received here will be turned into an enter
3735      * key down/up pair to go through the normal key handling.
3736      *
3737      * @param actionCode The code of the action being performed.
3738      *
3739      * @see #setOnEditorActionListener
3740      */
onEditorAction(int actionCode)3741     public void onEditorAction(int actionCode) {
3742         final InputContentType ict = mInputContentType;
3743         if (ict != null) {
3744             if (ict.onEditorActionListener != null) {
3745                 if (ict.onEditorActionListener.onEditorAction(this,
3746                         actionCode, null)) {
3747                     return;
3748                 }
3749             }
3750 
3751             // This is the handling for some default action.
3752             // Note that for backwards compatibility we don't do this
3753             // default handling if explicit ime options have not been given,
3754             // instead turning this into the normal enter key codes that an
3755             // app may be expecting.
3756             if (actionCode == EditorInfo.IME_ACTION_NEXT) {
3757                 View v = focusSearch(FOCUS_FORWARD);
3758                 if (v != null) {
3759                     if (!v.requestFocus(FOCUS_FORWARD)) {
3760                         throw new IllegalStateException("focus search returned a view " +
3761                                 "that wasn't able to take focus!");
3762                     }
3763                 }
3764                 return;
3765 
3766             } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
3767                 View v = focusSearch(FOCUS_BACKWARD);
3768                 if (v != null) {
3769                     if (!v.requestFocus(FOCUS_BACKWARD)) {
3770                         throw new IllegalStateException("focus search returned a view " +
3771                                 "that wasn't able to take focus!");
3772                     }
3773                 }
3774                 return;
3775 
3776             } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
3777                 InputMethodManager imm = InputMethodManager.peekInstance();
3778                 if (imm != null && imm.isActive(this)) {
3779                     imm.hideSoftInputFromWindow(getWindowToken(), 0);
3780                 }
3781                 clearFocus();
3782                 return;
3783             }
3784         }
3785 
3786         Handler h = getHandler();
3787         if (h != null) {
3788             long eventTime = SystemClock.uptimeMillis();
3789             h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
3790                     new KeyEvent(eventTime, eventTime,
3791                     KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
3792                     KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
3793                     KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3794                     | KeyEvent.FLAG_EDITOR_ACTION)));
3795             h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
3796                     new KeyEvent(SystemClock.uptimeMillis(), eventTime,
3797                     KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
3798                     KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
3799                     KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3800                     | KeyEvent.FLAG_EDITOR_ACTION)));
3801         }
3802     }
3803 
3804     /**
3805      * Set the private content type of the text, which is the
3806      * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
3807      * field that will be filled in when creating an input connection.
3808      *
3809      * @see #getPrivateImeOptions()
3810      * @see EditorInfo#privateImeOptions
3811      * @attr ref android.R.styleable#TextView_privateImeOptions
3812      */
setPrivateImeOptions(String type)3813     public void setPrivateImeOptions(String type) {
3814         if (mInputContentType == null) mInputContentType = new InputContentType();
3815         mInputContentType.privateImeOptions = type;
3816     }
3817 
3818     /**
3819      * Get the private type of the content.
3820      *
3821      * @see #setPrivateImeOptions(String)
3822      * @see EditorInfo#privateImeOptions
3823      */
getPrivateImeOptions()3824     public String getPrivateImeOptions() {
3825         return mInputContentType != null
3826                 ? mInputContentType.privateImeOptions : null;
3827     }
3828 
3829     /**
3830      * Set the extra input data of the text, which is the
3831      * {@link EditorInfo#extras TextBoxAttribute.extras}
3832      * Bundle that will be filled in when creating an input connection.  The
3833      * given integer is the resource ID of an XML resource holding an
3834      * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
3835      *
3836      * @see #getInputExtras(boolean)
3837      * @see EditorInfo#extras
3838      * @attr ref android.R.styleable#TextView_editorExtras
3839      */
setInputExtras(int xmlResId)3840     public void setInputExtras(int xmlResId)
3841             throws XmlPullParserException, IOException {
3842         XmlResourceParser parser = getResources().getXml(xmlResId);
3843         if (mInputContentType == null) mInputContentType = new InputContentType();
3844         mInputContentType.extras = new Bundle();
3845         getResources().parseBundleExtras(parser, mInputContentType.extras);
3846     }
3847 
3848     /**
3849      * Retrieve the input extras currently associated with the text view, which
3850      * can be viewed as well as modified.
3851      *
3852      * @param create If true, the extras will be created if they don't already
3853      * exist.  Otherwise, null will be returned if none have been created.
3854      * @see #setInputExtras(int)
3855      * @see EditorInfo#extras
3856      * @attr ref android.R.styleable#TextView_editorExtras
3857      */
getInputExtras(boolean create)3858     public Bundle getInputExtras(boolean create) {
3859         if (mInputContentType == null) {
3860             if (!create) return null;
3861             mInputContentType = new InputContentType();
3862         }
3863         if (mInputContentType.extras == null) {
3864             if (!create) return null;
3865             mInputContentType.extras = new Bundle();
3866         }
3867         return mInputContentType.extras;
3868     }
3869 
3870     /**
3871      * Returns the error message that was set to be displayed with
3872      * {@link #setError}, or <code>null</code> if no error was set
3873      * or if it the error was cleared by the widget after user input.
3874      */
getError()3875     public CharSequence getError() {
3876         return mError;
3877     }
3878 
3879     /**
3880      * Sets the right-hand compound drawable of the TextView to the "error"
3881      * icon and sets an error message that will be displayed in a popup when
3882      * the TextView has focus.  The icon and error message will be reset to
3883      * null when any key events cause changes to the TextView's text.  If the
3884      * <code>error</code> is <code>null</code>, the error message and icon
3885      * will be cleared.
3886      */
3887     @android.view.RemotableViewMethod
setError(CharSequence error)3888     public void setError(CharSequence error) {
3889         if (error == null) {
3890             setError(null, null);
3891         } else {
3892             Drawable dr = getContext().getResources().
3893                 getDrawable(com.android.internal.R.drawable.indicator_input_error);
3894 
3895             dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
3896             setError(error, dr);
3897         }
3898     }
3899 
3900     /**
3901      * Sets the right-hand compound drawable of the TextView to the specified
3902      * icon and sets an error message that will be displayed in a popup when
3903      * the TextView has focus.  The icon and error message will be reset to
3904      * null when any key events cause changes to the TextView's text.  The
3905      * drawable must already have had {@link Drawable#setBounds} set on it.
3906      * If the <code>error</code> is <code>null</code>, the error message will
3907      * be cleared (and you should provide a <code>null</code> icon as well).
3908      */
setError(CharSequence error, Drawable icon)3909     public void setError(CharSequence error, Drawable icon) {
3910         error = TextUtils.stringOrSpannedString(error);
3911 
3912         mError = error;
3913         mErrorWasChanged = true;
3914         final Drawables dr = mDrawables;
3915         if (dr != null) {
3916             switch (getResolvedLayoutDirection()) {
3917                 default:
3918                 case LAYOUT_DIRECTION_LTR:
3919                     setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
3920                             dr.mDrawableBottom);
3921                     break;
3922                 case LAYOUT_DIRECTION_RTL:
3923                     setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
3924                             dr.mDrawableBottom);
3925                     break;
3926             }
3927         } else {
3928             setCompoundDrawables(null, null, icon, null);
3929         }
3930 
3931         if (error == null) {
3932             if (mPopup != null) {
3933                 if (mPopup.isShowing()) {
3934                     mPopup.dismiss();
3935                 }
3936 
3937                 mPopup = null;
3938             }
3939         } else {
3940             if (isFocused()) {
3941                 showError();
3942             }
3943         }
3944     }
3945 
showError()3946     private void showError() {
3947         if (getWindowToken() == null) {
3948             mShowErrorAfterAttach = true;
3949             return;
3950         }
3951 
3952         if (mPopup == null) {
3953             LayoutInflater inflater = LayoutInflater.from(getContext());
3954             final TextView err = (TextView) inflater.inflate(
3955                     com.android.internal.R.layout.textview_hint, null);
3956 
3957             final float scale = getResources().getDisplayMetrics().density;
3958             mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
3959             mPopup.setFocusable(false);
3960             // The user is entering text, so the input method is needed.  We
3961             // don't want the popup to be displayed on top of it.
3962             mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
3963         }
3964 
3965         TextView tv = (TextView) mPopup.getContentView();
3966         chooseSize(mPopup, mError, tv);
3967         tv.setText(mError);
3968 
3969         mPopup.showAsDropDown(this, getErrorX(), getErrorY());
3970         mPopup.fixDirection(mPopup.isAboveAnchor());
3971     }
3972 
3973     private static class ErrorPopup extends PopupWindow {
3974         private boolean mAbove = false;
3975         private final TextView mView;
3976         private int mPopupInlineErrorBackgroundId = 0;
3977         private int mPopupInlineErrorAboveBackgroundId = 0;
3978 
ErrorPopup(TextView v, int width, int height)3979         ErrorPopup(TextView v, int width, int height) {
3980             super(v, width, height);
3981             mView = v;
3982             // Make sure the TextView has a background set as it will be used the first time it is
3983             // shown and positionned. Initialized with below background, which should have
3984             // dimensions identical to the above version for this to work (and is more likely).
3985             mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3986                     com.android.internal.R.styleable.Theme_errorMessageBackground);
3987             mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
3988         }
3989 
fixDirection(boolean above)3990         void fixDirection(boolean above) {
3991             mAbove = above;
3992 
3993             if (above) {
3994                 mPopupInlineErrorAboveBackgroundId =
3995                     getResourceId(mPopupInlineErrorAboveBackgroundId,
3996                             com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
3997             } else {
3998                 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3999                         com.android.internal.R.styleable.Theme_errorMessageBackground);
4000             }
4001 
4002             mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
4003                 mPopupInlineErrorBackgroundId);
4004         }
4005 
getResourceId(int currentId, int index)4006         private int getResourceId(int currentId, int index) {
4007             if (currentId == 0) {
4008                 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
4009                         R.styleable.Theme);
4010                 currentId = styledAttributes.getResourceId(index, 0);
4011                 styledAttributes.recycle();
4012             }
4013             return currentId;
4014         }
4015 
4016         @Override
update(int x, int y, int w, int h, boolean force)4017         public void update(int x, int y, int w, int h, boolean force) {
4018             super.update(x, y, w, h, force);
4019 
4020             boolean above = isAboveAnchor();
4021             if (above != mAbove) {
4022                 fixDirection(above);
4023             }
4024         }
4025     }
4026 
4027     /**
4028      * Returns the Y offset to make the pointy top of the error point
4029      * at the middle of the error icon.
4030      */
getErrorX()4031     private int getErrorX() {
4032         /*
4033          * The "25" is the distance between the point and the right edge
4034          * of the background
4035          */
4036         final float scale = getResources().getDisplayMetrics().density;
4037 
4038         final Drawables dr = mDrawables;
4039         return getWidth() - mPopup.getWidth() - getPaddingRight() -
4040                 (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
4041     }
4042 
4043     /**
4044      * Returns the Y offset to make the pointy top of the error point
4045      * at the bottom of the error icon.
4046      */
getErrorY()4047     private int getErrorY() {
4048         /*
4049          * Compound, not extended, because the icon is not clipped
4050          * if the text height is smaller.
4051          */
4052         final int compoundPaddingTop = getCompoundPaddingTop();
4053         int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
4054 
4055         final Drawables dr = mDrawables;
4056         int icontop = compoundPaddingTop +
4057                 (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
4058 
4059         /*
4060          * The "2" is the distance between the point and the top edge
4061          * of the background.
4062          */
4063         final float scale = getResources().getDisplayMetrics().density;
4064         return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
4065                 (int) (2 * scale + 0.5f);
4066     }
4067 
hideError()4068     private void hideError() {
4069         if (mPopup != null) {
4070             if (mPopup.isShowing()) {
4071                 mPopup.dismiss();
4072             }
4073         }
4074 
4075         mShowErrorAfterAttach = false;
4076     }
4077 
chooseSize(PopupWindow pop, CharSequence text, TextView tv)4078     private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
4079         int wid = tv.getPaddingLeft() + tv.getPaddingRight();
4080         int ht = tv.getPaddingTop() + tv.getPaddingBottom();
4081 
4082         int defaultWidthInPixels = getResources().getDimensionPixelSize(
4083                 com.android.internal.R.dimen.textview_error_popup_default_width);
4084         Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
4085                                     Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
4086         float max = 0;
4087         for (int i = 0; i < l.getLineCount(); i++) {
4088             max = Math.max(max, l.getLineWidth(i));
4089         }
4090 
4091         /*
4092          * Now set the popup size to be big enough for the text plus the border capped
4093          * to DEFAULT_MAX_POPUP_WIDTH
4094          */
4095         pop.setWidth(wid + (int) Math.ceil(max));
4096         pop.setHeight(ht + l.getHeight());
4097     }
4098 
4099 
4100     @Override
setFrame(int l, int t, int r, int b)4101     protected boolean setFrame(int l, int t, int r, int b) {
4102         boolean result = super.setFrame(l, t, r, b);
4103 
4104         if (mPopup != null) {
4105             TextView tv = (TextView) mPopup.getContentView();
4106             chooseSize(mPopup, mError, tv);
4107             mPopup.update(this, getErrorX(), getErrorY(),
4108                           mPopup.getWidth(), mPopup.getHeight());
4109         }
4110 
4111         restartMarqueeIfNeeded();
4112 
4113         return result;
4114     }
4115 
restartMarqueeIfNeeded()4116     private void restartMarqueeIfNeeded() {
4117         if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
4118             mRestartMarquee = false;
4119             startMarquee();
4120         }
4121     }
4122 
4123     /**
4124      * Sets the list of input filters that will be used if the buffer is
4125      * Editable.  Has no effect otherwise.
4126      *
4127      * @attr ref android.R.styleable#TextView_maxLength
4128      */
setFilters(InputFilter[] filters)4129     public void setFilters(InputFilter[] filters) {
4130         if (filters == null) {
4131             throw new IllegalArgumentException();
4132         }
4133 
4134         mFilters = filters;
4135 
4136         if (mText instanceof Editable) {
4137             setFilters((Editable) mText, filters);
4138         }
4139     }
4140 
4141     /**
4142      * Sets the list of input filters on the specified Editable,
4143      * and includes mInput in the list if it is an InputFilter.
4144      */
setFilters(Editable e, InputFilter[] filters)4145     private void setFilters(Editable e, InputFilter[] filters) {
4146         if (mInput instanceof InputFilter) {
4147             InputFilter[] nf = new InputFilter[filters.length + 1];
4148 
4149             System.arraycopy(filters, 0, nf, 0, filters.length);
4150             nf[filters.length] = (InputFilter) mInput;
4151 
4152             e.setFilters(nf);
4153         } else {
4154             e.setFilters(filters);
4155         }
4156     }
4157 
4158     /**
4159      * Returns the current list of input filters.
4160      */
getFilters()4161     public InputFilter[] getFilters() {
4162         return mFilters;
4163     }
4164 
4165     /////////////////////////////////////////////////////////////////////////
4166 
getVerticalOffset(boolean forceNormal)4167     private int getVerticalOffset(boolean forceNormal) {
4168         int voffset = 0;
4169         final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
4170 
4171         Layout l = mLayout;
4172         if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
4173             l = mHintLayout;
4174         }
4175 
4176         if (gravity != Gravity.TOP) {
4177             int boxht;
4178 
4179             if (l == mHintLayout) {
4180                 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
4181                         getCompoundPaddingBottom();
4182             } else {
4183                 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
4184                         getExtendedPaddingBottom();
4185             }
4186             int textht = l.getHeight();
4187 
4188             if (textht < boxht) {
4189                 if (gravity == Gravity.BOTTOM)
4190                     voffset = boxht - textht;
4191                 else // (gravity == Gravity.CENTER_VERTICAL)
4192                     voffset = (boxht - textht) >> 1;
4193             }
4194         }
4195         return voffset;
4196     }
4197 
getBottomVerticalOffset(boolean forceNormal)4198     private int getBottomVerticalOffset(boolean forceNormal) {
4199         int voffset = 0;
4200         final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
4201 
4202         Layout l = mLayout;
4203         if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
4204             l = mHintLayout;
4205         }
4206 
4207         if (gravity != Gravity.BOTTOM) {
4208             int boxht;
4209 
4210             if (l == mHintLayout) {
4211                 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
4212                         getCompoundPaddingBottom();
4213             } else {
4214                 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
4215                         getExtendedPaddingBottom();
4216             }
4217             int textht = l.getHeight();
4218 
4219             if (textht < boxht) {
4220                 if (gravity == Gravity.TOP)
4221                     voffset = boxht - textht;
4222                 else // (gravity == Gravity.CENTER_VERTICAL)
4223                     voffset = (boxht - textht) >> 1;
4224             }
4225         }
4226         return voffset;
4227     }
4228 
invalidateCursorPath()4229     private void invalidateCursorPath() {
4230         if (mHighlightPathBogus) {
4231             invalidateCursor();
4232         } else {
4233             final int horizontalPadding = getCompoundPaddingLeft();
4234             final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
4235 
4236             if (mCursorCount == 0) {
4237                 synchronized (sTempRect) {
4238                     /*
4239                      * The reason for this concern about the thickness of the
4240                      * cursor and doing the floor/ceil on the coordinates is that
4241                      * some EditTexts (notably textfields in the Browser) have
4242                      * anti-aliased text where not all the characters are
4243                      * necessarily at integer-multiple locations.  This should
4244                      * make sure the entire cursor gets invalidated instead of
4245                      * sometimes missing half a pixel.
4246                      */
4247                     float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
4248                     if (thick < 1.0f) {
4249                         thick = 1.0f;
4250                     }
4251 
4252                     thick /= 2.0f;
4253 
4254                     mHighlightPath.computeBounds(sTempRect, false);
4255 
4256                     invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick),
4257                             (int) FloatMath.floor(verticalPadding + sTempRect.top - thick),
4258                             (int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick),
4259                             (int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick));
4260                 }
4261             } else {
4262                 for (int i = 0; i < mCursorCount; i++) {
4263                     Rect bounds = mCursorDrawable[i].getBounds();
4264                     invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
4265                             bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
4266                 }
4267             }
4268         }
4269     }
4270 
invalidateCursor()4271     private void invalidateCursor() {
4272         int where = getSelectionEnd();
4273 
4274         invalidateCursor(where, where, where);
4275     }
4276 
invalidateCursor(int a, int b, int c)4277     private void invalidateCursor(int a, int b, int c) {
4278         if (mLayout == null) {
4279             invalidate();
4280         } else {
4281             if (a >= 0 || b >= 0 || c >= 0) {
4282                 int first = Math.min(Math.min(a, b), c);
4283                 int last = Math.max(Math.max(a, b), c);
4284 
4285                 int line = mLayout.getLineForOffset(first);
4286                 int top = mLayout.getLineTop(line);
4287 
4288                 // This is ridiculous, but the descent from the line above
4289                 // can hang down into the line we really want to redraw,
4290                 // so we have to invalidate part of the line above to make
4291                 // sure everything that needs to be redrawn really is.
4292                 // (But not the whole line above, because that would cause
4293                 // the same problem with the descenders on the line above it!)
4294                 if (line > 0) {
4295                     top -= mLayout.getLineDescent(line - 1);
4296                 }
4297 
4298                 int line2;
4299 
4300                 if (first == last)
4301                     line2 = line;
4302                 else
4303                     line2 = mLayout.getLineForOffset(last);
4304 
4305                 int bottom = mLayout.getLineTop(line2 + 1);
4306 
4307                 final int horizontalPadding = getCompoundPaddingLeft();
4308                 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
4309 
4310                 // If used, the cursor drawables can have an arbitrary dimension that can go beyond
4311                 // the invalidated lines specified above.
4312                 for (int i = 0; i < mCursorCount; i++) {
4313                     Rect bounds = mCursorDrawable[i].getBounds();
4314                     top = Math.min(top, bounds.top);
4315                     bottom = Math.max(bottom, bounds.bottom);
4316                     // Horizontal bounds are already full width, no need to update
4317                 }
4318 
4319                 invalidate(horizontalPadding + mScrollX, top + verticalPadding,
4320                         horizontalPadding + mScrollX + getWidth() -
4321                         getCompoundPaddingLeft() - getCompoundPaddingRight(),
4322                         bottom + verticalPadding);
4323             }
4324         }
4325     }
4326 
registerForPreDraw()4327     private void registerForPreDraw() {
4328         final ViewTreeObserver observer = getViewTreeObserver();
4329 
4330         if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
4331             observer.addOnPreDrawListener(this);
4332             mPreDrawState = PREDRAW_PENDING;
4333         } else if (mPreDrawState == PREDRAW_DONE) {
4334             mPreDrawState = PREDRAW_PENDING;
4335         }
4336 
4337         // else state is PREDRAW_PENDING, so keep waiting.
4338     }
4339 
4340     /**
4341      * {@inheritDoc}
4342      */
onPreDraw()4343     public boolean onPreDraw() {
4344         if (mPreDrawState != PREDRAW_PENDING) {
4345             return true;
4346         }
4347 
4348         if (mLayout == null) {
4349             assumeLayout();
4350         }
4351 
4352         boolean changed = false;
4353 
4354         if (mMovement != null) {
4355             /* This code also provides auto-scrolling when a cursor is moved using a
4356              * CursorController (insertion point or selection limits).
4357              * For selection, ensure start or end is visible depending on controller's state.
4358              */
4359             int curs = getSelectionEnd();
4360             // Do not create the controller if it is not already created.
4361             if (mSelectionModifierCursorController != null &&
4362                     mSelectionModifierCursorController.isSelectionStartDragged()) {
4363                 curs = getSelectionStart();
4364             }
4365 
4366             /*
4367              * TODO: This should really only keep the end in view if
4368              * it already was before the text changed.  I'm not sure
4369              * of a good way to tell from here if it was.
4370              */
4371             if (curs < 0 &&
4372                   (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
4373                 curs = mText.length();
4374             }
4375 
4376             if (curs >= 0) {
4377                 changed = bringPointIntoView(curs);
4378             }
4379         } else {
4380             changed = bringTextIntoView();
4381         }
4382 
4383         // This has to be checked here since:
4384         // - onFocusChanged cannot start it when focus is given to a view with selected text (after
4385         //   a screen rotation) since layout is not yet initialized at that point.
4386         if (mCreatedWithASelection) {
4387             startSelectionActionMode();
4388             mCreatedWithASelection = false;
4389         }
4390 
4391         // Phone specific code (there is no ExtractEditText on tablets).
4392         // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
4393         // not be set. Do the test here instead.
4394         if (this instanceof ExtractEditText && hasSelection()) {
4395             startSelectionActionMode();
4396         }
4397 
4398         mPreDrawState = PREDRAW_DONE;
4399         return !changed;
4400     }
4401 
4402     @Override
onAttachedToWindow()4403     protected void onAttachedToWindow() {
4404         super.onAttachedToWindow();
4405 
4406         mTemporaryDetach = false;
4407 
4408         if (mShowErrorAfterAttach) {
4409             showError();
4410             mShowErrorAfterAttach = false;
4411         }
4412 
4413         final ViewTreeObserver observer = getViewTreeObserver();
4414         // No need to create the controller.
4415         // The get method will add the listener on controller creation.
4416         if (mInsertionPointCursorController != null) {
4417             observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
4418         }
4419         if (mSelectionModifierCursorController != null) {
4420             observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
4421         }
4422 
4423         // Resolve drawables as the layout direction has been resolved
4424         resolveDrawables();
4425 
4426         updateSpellCheckSpans(0, mText.length());
4427     }
4428 
4429     @Override
onDetachedFromWindow()4430     protected void onDetachedFromWindow() {
4431         super.onDetachedFromWindow();
4432 
4433         final ViewTreeObserver observer = getViewTreeObserver();
4434         if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
4435             observer.removeOnPreDrawListener(this);
4436             mPreDrawState = PREDRAW_NOT_REGISTERED;
4437         }
4438 
4439         if (mError != null) {
4440             hideError();
4441         }
4442 
4443         if (mBlink != null) {
4444             mBlink.removeCallbacks(mBlink);
4445         }
4446 
4447         if (mInsertionPointCursorController != null) {
4448             mInsertionPointCursorController.onDetached();
4449         }
4450 
4451         if (mSelectionModifierCursorController != null) {
4452             mSelectionModifierCursorController.onDetached();
4453         }
4454 
4455         hideControllers();
4456 
4457         resetResolvedDrawables();
4458 
4459         if (mSpellChecker != null) {
4460             mSpellChecker.closeSession();
4461             // Forces the creation of a new SpellChecker next time this window is created.
4462             // Will handle the cases where the settings has been changed in the meantime.
4463             mSpellChecker = null;
4464         }
4465     }
4466 
4467     @Override
isPaddingOffsetRequired()4468     protected boolean isPaddingOffsetRequired() {
4469         return mShadowRadius != 0 || mDrawables != null;
4470     }
4471 
4472     @Override
getLeftPaddingOffset()4473     protected int getLeftPaddingOffset() {
4474         return getCompoundPaddingLeft() - mPaddingLeft +
4475                 (int) Math.min(0, mShadowDx - mShadowRadius);
4476     }
4477 
4478     @Override
getTopPaddingOffset()4479     protected int getTopPaddingOffset() {
4480         return (int) Math.min(0, mShadowDy - mShadowRadius);
4481     }
4482 
4483     @Override
getBottomPaddingOffset()4484     protected int getBottomPaddingOffset() {
4485         return (int) Math.max(0, mShadowDy + mShadowRadius);
4486     }
4487 
4488     @Override
getRightPaddingOffset()4489     protected int getRightPaddingOffset() {
4490         return -(getCompoundPaddingRight() - mPaddingRight) +
4491                 (int) Math.max(0, mShadowDx + mShadowRadius);
4492     }
4493 
4494     @Override
verifyDrawable(Drawable who)4495     protected boolean verifyDrawable(Drawable who) {
4496         final boolean verified = super.verifyDrawable(who);
4497         if (!verified && mDrawables != null) {
4498             return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
4499                     who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom ||
4500                     who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd;
4501         }
4502         return verified;
4503     }
4504 
4505     @Override
jumpDrawablesToCurrentState()4506     public void jumpDrawablesToCurrentState() {
4507         super.jumpDrawablesToCurrentState();
4508         if (mDrawables != null) {
4509             if (mDrawables.mDrawableLeft != null) {
4510                 mDrawables.mDrawableLeft.jumpToCurrentState();
4511             }
4512             if (mDrawables.mDrawableTop != null) {
4513                 mDrawables.mDrawableTop.jumpToCurrentState();
4514             }
4515             if (mDrawables.mDrawableRight != null) {
4516                 mDrawables.mDrawableRight.jumpToCurrentState();
4517             }
4518             if (mDrawables.mDrawableBottom != null) {
4519                 mDrawables.mDrawableBottom.jumpToCurrentState();
4520             }
4521             if (mDrawables.mDrawableStart != null) {
4522                 mDrawables.mDrawableStart.jumpToCurrentState();
4523             }
4524             if (mDrawables.mDrawableEnd != null) {
4525                 mDrawables.mDrawableEnd.jumpToCurrentState();
4526             }
4527         }
4528     }
4529 
4530     @Override
invalidateDrawable(Drawable drawable)4531     public void invalidateDrawable(Drawable drawable) {
4532         if (verifyDrawable(drawable)) {
4533             final Rect dirty = drawable.getBounds();
4534             int scrollX = mScrollX;
4535             int scrollY = mScrollY;
4536 
4537             // IMPORTANT: The coordinates below are based on the coordinates computed
4538             // for each compound drawable in onDraw(). Make sure to update each section
4539             // accordingly.
4540             final TextView.Drawables drawables = mDrawables;
4541             if (drawables != null) {
4542                 if (drawable == drawables.mDrawableLeft) {
4543                     final int compoundPaddingTop = getCompoundPaddingTop();
4544                     final int compoundPaddingBottom = getCompoundPaddingBottom();
4545                     final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
4546 
4547                     scrollX += mPaddingLeft;
4548                     scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
4549                 } else if (drawable == drawables.mDrawableRight) {
4550                     final int compoundPaddingTop = getCompoundPaddingTop();
4551                     final int compoundPaddingBottom = getCompoundPaddingBottom();
4552                     final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
4553 
4554                     scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
4555                     scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
4556                 } else if (drawable == drawables.mDrawableTop) {
4557                     final int compoundPaddingLeft = getCompoundPaddingLeft();
4558                     final int compoundPaddingRight = getCompoundPaddingRight();
4559                     final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
4560 
4561                     scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
4562                     scrollY += mPaddingTop;
4563                 } else if (drawable == drawables.mDrawableBottom) {
4564                     final int compoundPaddingLeft = getCompoundPaddingLeft();
4565                     final int compoundPaddingRight = getCompoundPaddingRight();
4566                     final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
4567 
4568                     scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
4569                     scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
4570                 }
4571             }
4572 
4573             invalidate(dirty.left + scrollX, dirty.top + scrollY,
4574                     dirty.right + scrollX, dirty.bottom + scrollY);
4575         }
4576     }
4577 
4578     /**
4579      * @hide
4580      */
4581     @Override
getResolvedLayoutDirection(Drawable who)4582     public int getResolvedLayoutDirection(Drawable who) {
4583         if (who == null) return View.LAYOUT_DIRECTION_LTR;
4584         if (mDrawables != null) {
4585             final Drawables drawables = mDrawables;
4586             if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight ||
4587                 who == drawables.mDrawableTop || who == drawables.mDrawableBottom ||
4588                 who == drawables.mDrawableStart || who == drawables.mDrawableEnd) {
4589                 return getResolvedLayoutDirection();
4590             }
4591         }
4592         return super.getResolvedLayoutDirection(who);
4593     }
4594 
4595     @Override
onSetAlpha(int alpha)4596     protected boolean onSetAlpha(int alpha) {
4597         // Alpha is supported if and only if the drawing can be done in one pass.
4598         // TODO text with spans with a background color currently do not respect this alpha.
4599         if (getBackground() == null) {
4600             mCurrentAlpha = alpha;
4601             final Drawables dr = mDrawables;
4602             if (dr != null) {
4603                 if (dr.mDrawableLeft != null) dr.mDrawableLeft.mutate().setAlpha(alpha);
4604                 if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha);
4605                 if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha);
4606                 if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha);
4607                 if (dr.mDrawableStart != null) dr.mDrawableStart.mutate().setAlpha(alpha);
4608                 if (dr.mDrawableEnd != null) dr.mDrawableEnd.mutate().setAlpha(alpha);
4609             }
4610             return true;
4611         }
4612 
4613         mCurrentAlpha = 255;
4614         return false;
4615     }
4616 
4617     /**
4618      * When a TextView is used to display a useful piece of information to the user (such as a
4619      * contact's address), it should be made selectable, so that the user can select and copy this
4620      * content.
4621      *
4622      * Use {@link #setTextIsSelectable(boolean)} or the
4623      * {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView
4624      * selectable (text is not selectable by default).
4625      *
4626      * Note that this method simply returns the state of this flag. Although this flag has to be set
4627      * in order to select text in non-editable TextView, the content of an {@link EditText} can
4628      * always be selected, independently of the value of this flag.
4629      *
4630      * @return True if the text displayed in this TextView can be selected by the user.
4631      *
4632      * @attr ref android.R.styleable#TextView_textIsSelectable
4633      */
isTextSelectable()4634     public boolean isTextSelectable() {
4635         return mTextIsSelectable;
4636     }
4637 
4638     /**
4639      * Sets whether or not (default) the content of this view is selectable by the user.
4640      *
4641      * Note that this methods affect the {@link #setFocusable(boolean)},
4642      * {@link #setFocusableInTouchMode(boolean)} {@link #setClickable(boolean)} and
4643      * {@link #setLongClickable(boolean)} states and you may want to restore these if they were
4644      * customized.
4645      *
4646      * See {@link #isTextSelectable} for details.
4647      *
4648      * @param selectable Whether or not the content of this TextView should be selectable.
4649      */
setTextIsSelectable(boolean selectable)4650     public void setTextIsSelectable(boolean selectable) {
4651         if (mTextIsSelectable == selectable) return;
4652 
4653         mTextIsSelectable = selectable;
4654 
4655         setFocusableInTouchMode(selectable);
4656         setFocusable(selectable);
4657         setClickable(selectable);
4658         setLongClickable(selectable);
4659 
4660         // mInputType is already EditorInfo.TYPE_NULL and mInput is null;
4661 
4662         setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
4663         setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
4664 
4665         // Called by setText above, but safer in case of future code changes
4666         prepareCursorControllers();
4667     }
4668 
4669     @Override
onCreateDrawableState(int extraSpace)4670     protected int[] onCreateDrawableState(int extraSpace) {
4671         final int[] drawableState;
4672 
4673         if (mSingleLine) {
4674             drawableState = super.onCreateDrawableState(extraSpace);
4675         } else {
4676             drawableState = super.onCreateDrawableState(extraSpace + 1);
4677             mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
4678         }
4679 
4680         if (mTextIsSelectable) {
4681             // Disable pressed state, which was introduced when TextView was made clickable.
4682             // Prevents text color change.
4683             // setClickable(false) would have a similar effect, but it also disables focus changes
4684             // and long press actions, which are both needed by text selection.
4685             final int length = drawableState.length;
4686             for (int i = 0; i < length; i++) {
4687                 if (drawableState[i] == R.attr.state_pressed) {
4688                     final int[] nonPressedState = new int[length - 1];
4689                     System.arraycopy(drawableState, 0, nonPressedState, 0, i);
4690                     System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
4691                     return nonPressedState;
4692                 }
4693             }
4694         }
4695 
4696         return drawableState;
4697     }
4698 
4699     @Override
onDraw(Canvas canvas)4700     protected void onDraw(Canvas canvas) {
4701         if (mPreDrawState == PREDRAW_DONE) {
4702             final ViewTreeObserver observer = getViewTreeObserver();
4703             observer.removeOnPreDrawListener(this);
4704             mPreDrawState = PREDRAW_NOT_REGISTERED;
4705         }
4706 
4707         if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
4708 
4709         restartMarqueeIfNeeded();
4710 
4711         // Draw the background for this view
4712         super.onDraw(canvas);
4713 
4714         final int compoundPaddingLeft = getCompoundPaddingLeft();
4715         final int compoundPaddingTop = getCompoundPaddingTop();
4716         final int compoundPaddingRight = getCompoundPaddingRight();
4717         final int compoundPaddingBottom = getCompoundPaddingBottom();
4718         final int scrollX = mScrollX;
4719         final int scrollY = mScrollY;
4720         final int right = mRight;
4721         final int left = mLeft;
4722         final int bottom = mBottom;
4723         final int top = mTop;
4724 
4725         final Drawables dr = mDrawables;
4726         if (dr != null) {
4727             /*
4728              * Compound, not extended, because the icon is not clipped
4729              * if the text height is smaller.
4730              */
4731 
4732             int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
4733             int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
4734 
4735             // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4736             // Make sure to update invalidateDrawable() when changing this code.
4737             if (dr.mDrawableLeft != null) {
4738                 canvas.save();
4739                 canvas.translate(scrollX + mPaddingLeft,
4740                                  scrollY + compoundPaddingTop +
4741                                  (vspace - dr.mDrawableHeightLeft) / 2);
4742                 dr.mDrawableLeft.draw(canvas);
4743                 canvas.restore();
4744             }
4745 
4746             // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4747             // Make sure to update invalidateDrawable() when changing this code.
4748             if (dr.mDrawableRight != null) {
4749                 canvas.save();
4750                 canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
4751                          scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
4752                 dr.mDrawableRight.draw(canvas);
4753                 canvas.restore();
4754             }
4755 
4756             // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4757             // Make sure to update invalidateDrawable() when changing this code.
4758             if (dr.mDrawableTop != null) {
4759                 canvas.save();
4760                 canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
4761                         scrollY + mPaddingTop);
4762                 dr.mDrawableTop.draw(canvas);
4763                 canvas.restore();
4764             }
4765 
4766             // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4767             // Make sure to update invalidateDrawable() when changing this code.
4768             if (dr.mDrawableBottom != null) {
4769                 canvas.save();
4770                 canvas.translate(scrollX + compoundPaddingLeft +
4771                         (hspace - dr.mDrawableWidthBottom) / 2,
4772                          scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
4773                 dr.mDrawableBottom.draw(canvas);
4774                 canvas.restore();
4775             }
4776         }
4777 
4778         int color = mCurTextColor;
4779 
4780         if (mLayout == null) {
4781             assumeLayout();
4782         }
4783 
4784         Layout layout = mLayout;
4785         int cursorcolor = color;
4786 
4787         if (mHint != null && mText.length() == 0) {
4788             if (mHintTextColor != null) {
4789                 color = mCurHintTextColor;
4790             }
4791 
4792             layout = mHintLayout;
4793         }
4794 
4795         mTextPaint.setColor(color);
4796         if (mCurrentAlpha != 255) {
4797             // If set, the alpha will override the color's alpha. Multiply the alphas.
4798             mTextPaint.setAlpha((mCurrentAlpha * Color.alpha(color)) / 255);
4799         }
4800         mTextPaint.drawableState = getDrawableState();
4801 
4802         canvas.save();
4803         /*  Would be faster if we didn't have to do this. Can we chop the
4804             (displayable) text so that we don't need to do this ever?
4805         */
4806 
4807         int extendedPaddingTop = getExtendedPaddingTop();
4808         int extendedPaddingBottom = getExtendedPaddingBottom();
4809 
4810         float clipLeft = compoundPaddingLeft + scrollX;
4811         float clipTop = extendedPaddingTop + scrollY;
4812         float clipRight = right - left - compoundPaddingRight + scrollX;
4813         float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
4814 
4815         if (mShadowRadius != 0) {
4816             clipLeft += Math.min(0, mShadowDx - mShadowRadius);
4817             clipRight += Math.max(0, mShadowDx + mShadowRadius);
4818 
4819             clipTop += Math.min(0, mShadowDy - mShadowRadius);
4820             clipBottom += Math.max(0, mShadowDy + mShadowRadius);
4821         }
4822 
4823         canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
4824 
4825         int voffsetText = 0;
4826         int voffsetCursor = 0;
4827 
4828         // translate in by our padding
4829         {
4830             /* shortcircuit calling getVerticaOffset() */
4831             if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4832                 voffsetText = getVerticalOffset(false);
4833                 voffsetCursor = getVerticalOffset(true);
4834             }
4835             canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
4836         }
4837 
4838         final int layoutDirection = getResolvedLayoutDirection();
4839         final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
4840         if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
4841                 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
4842             if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
4843                     (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
4844                 canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
4845                         getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
4846             }
4847 
4848             if (mMarquee != null && mMarquee.isRunning()) {
4849                 canvas.translate(-mMarquee.mScroll, 0.0f);
4850             }
4851         }
4852 
4853         Path highlight = null;
4854         int selStart = -1, selEnd = -1;
4855         boolean drawCursor = false;
4856 
4857         //  If there is no movement method, then there can be no selection.
4858         //  Check that first and attempt to skip everything having to do with
4859         //  the cursor.
4860         //  XXX This is not strictly true -- a program could set the
4861         //  selection manually if it really wanted to.
4862         if (mMovement != null && (isFocused() || isPressed())) {
4863             selStart = getSelectionStart();
4864             selEnd = getSelectionEnd();
4865 
4866             if (selStart >= 0) {
4867                 if (mHighlightPath == null) mHighlightPath = new Path();
4868 
4869                 if (selStart == selEnd) {
4870                     if (isCursorVisible() &&
4871                             (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
4872                         if (mHighlightPathBogus) {
4873                             mHighlightPath.reset();
4874                             mLayout.getCursorPath(selStart, mHighlightPath, mText);
4875                             updateCursorsPositions();
4876                             mHighlightPathBogus = false;
4877                         }
4878 
4879                         // XXX should pass to skin instead of drawing directly
4880                         mHighlightPaint.setColor(cursorcolor);
4881                         if (mCurrentAlpha != 255) {
4882                             mHighlightPaint.setAlpha(
4883                                     (mCurrentAlpha * Color.alpha(cursorcolor)) / 255);
4884                         }
4885                         mHighlightPaint.setStyle(Paint.Style.STROKE);
4886                         highlight = mHighlightPath;
4887                         drawCursor = mCursorCount > 0;
4888                     }
4889                 } else if (textCanBeSelected()) {
4890                     if (mHighlightPathBogus) {
4891                         mHighlightPath.reset();
4892                         mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
4893                         mHighlightPathBogus = false;
4894                     }
4895 
4896                     // XXX should pass to skin instead of drawing directly
4897                     mHighlightPaint.setColor(mHighlightColor);
4898                     if (mCurrentAlpha != 255) {
4899                         mHighlightPaint.setAlpha(
4900                                 (mCurrentAlpha * Color.alpha(mHighlightColor)) / 255);
4901                     }
4902                     mHighlightPaint.setStyle(Paint.Style.FILL);
4903 
4904                     highlight = mHighlightPath;
4905                 }
4906             }
4907         }
4908 
4909         /*  Comment out until we decide what to do about animations
4910         boolean isLinearTextOn = false;
4911         if (currentTransformation != null) {
4912             isLinearTextOn = mTextPaint.isLinearTextOn();
4913             Matrix m = currentTransformation.getMatrix();
4914             if (!m.isIdentity()) {
4915                 // mTextPaint.setLinearTextOn(true);
4916             }
4917         }
4918         */
4919 
4920         final InputMethodState ims = mInputMethodState;
4921         final int cursorOffsetVertical = voffsetCursor - voffsetText;
4922         if (ims != null && ims.mBatchEditNesting == 0) {
4923             InputMethodManager imm = InputMethodManager.peekInstance();
4924             if (imm != null) {
4925                 if (imm.isActive(this)) {
4926                     boolean reported = false;
4927                     if (ims.mContentChanged || ims.mSelectionModeChanged) {
4928                         // We are in extract mode and the content has changed
4929                         // in some way... just report complete new text to the
4930                         // input method.
4931                         reported = reportExtractedText();
4932                     }
4933                     if (!reported && highlight != null) {
4934                         int candStart = -1;
4935                         int candEnd = -1;
4936                         if (mText instanceof Spannable) {
4937                             Spannable sp = (Spannable)mText;
4938                             candStart = EditableInputConnection.getComposingSpanStart(sp);
4939                             candEnd = EditableInputConnection.getComposingSpanEnd(sp);
4940                         }
4941                         imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
4942                     }
4943                 }
4944 
4945                 if (imm.isWatchingCursor(this) && highlight != null) {
4946                     highlight.computeBounds(ims.mTmpRectF, true);
4947                     ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
4948 
4949                     canvas.getMatrix().mapPoints(ims.mTmpOffset);
4950                     ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
4951 
4952                     ims.mTmpRectF.offset(0, cursorOffsetVertical);
4953 
4954                     ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
4955                             (int)(ims.mTmpRectF.top + 0.5),
4956                             (int)(ims.mTmpRectF.right + 0.5),
4957                             (int)(ims.mTmpRectF.bottom + 0.5));
4958 
4959                     imm.updateCursor(this,
4960                             ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
4961                             ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
4962                 }
4963             }
4964         }
4965 
4966         if (mCorrectionHighlighter != null) {
4967             mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
4968         }
4969 
4970         if (drawCursor) {
4971             drawCursor(canvas, cursorOffsetVertical);
4972             // Rely on the drawable entirely, do not draw the cursor line.
4973             // Has to be done after the IMM related code above which relies on the highlight.
4974             highlight = null;
4975         }
4976 
4977         layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
4978 
4979         if (mMarquee != null && mMarquee.shouldDrawGhost()) {
4980             canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
4981             layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
4982         }
4983 
4984         /*  Comment out until we decide what to do about animations
4985         if (currentTransformation != null) {
4986             mTextPaint.setLinearTextOn(isLinearTextOn);
4987         }
4988         */
4989 
4990         canvas.restore();
4991     }
4992 
updateCursorsPositions()4993     private void updateCursorsPositions() {
4994         if (mCursorDrawableRes == 0) {
4995             mCursorCount = 0;
4996             return;
4997         }
4998 
4999         final int offset = getSelectionStart();
5000         final int line = mLayout.getLineForOffset(offset);
5001         final int top = mLayout.getLineTop(line);
5002         final int bottom = mLayout.getLineTop(line + 1);
5003 
5004         mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
5005 
5006         int middle = bottom;
5007         if (mCursorCount == 2) {
5008             // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
5009             middle = (top + bottom) >> 1;
5010         }
5011 
5012         updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
5013 
5014         if (mCursorCount == 2) {
5015             updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
5016         }
5017     }
5018 
updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal)5019     private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
5020         if (mCursorDrawable[cursorIndex] == null)
5021             mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
5022 
5023         if (mTempRect == null) mTempRect = new Rect();
5024 
5025         mCursorDrawable[cursorIndex].getPadding(mTempRect);
5026         final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
5027         horizontal = Math.max(0.5f, horizontal - 0.5f);
5028         final int left = (int) (horizontal) - mTempRect.left;
5029         mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
5030                 bottom + mTempRect.bottom);
5031     }
5032 
drawCursor(Canvas canvas, int cursorOffsetVertical)5033     private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
5034         final boolean translate = cursorOffsetVertical != 0;
5035         if (translate) canvas.translate(0, cursorOffsetVertical);
5036         for (int i = 0; i < mCursorCount; i++) {
5037             mCursorDrawable[i].draw(canvas);
5038         }
5039         if (translate) canvas.translate(0, -cursorOffsetVertical);
5040     }
5041 
5042     @Override
getFocusedRect(Rect r)5043     public void getFocusedRect(Rect r) {
5044         if (mLayout == null) {
5045             super.getFocusedRect(r);
5046             return;
5047         }
5048 
5049         int selEnd = getSelectionEnd();
5050         if (selEnd < 0) {
5051             super.getFocusedRect(r);
5052             return;
5053         }
5054 
5055         int selStart = getSelectionStart();
5056         if (selStart < 0 || selStart >= selEnd) {
5057             int line = mLayout.getLineForOffset(selEnd);
5058             r.top = mLayout.getLineTop(line);
5059             r.bottom = mLayout.getLineBottom(line);
5060             r.left = (int) mLayout.getPrimaryHorizontal(selEnd) - 2;
5061             r.right = r.left + 4;
5062         } else {
5063             int lineStart = mLayout.getLineForOffset(selStart);
5064             int lineEnd = mLayout.getLineForOffset(selEnd);
5065             r.top = mLayout.getLineTop(lineStart);
5066             r.bottom = mLayout.getLineBottom(lineEnd);
5067             if (lineStart == lineEnd) {
5068                 r.left = (int) mLayout.getPrimaryHorizontal(selStart);
5069                 r.right = (int) mLayout.getPrimaryHorizontal(selEnd);
5070             } else {
5071                 // Selection extends across multiple lines -- the focused
5072                 // rect covers the entire width.
5073                 if (mHighlightPath == null) mHighlightPath = new Path();
5074                 if (mHighlightPathBogus) {
5075                     mHighlightPath.reset();
5076                     mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
5077                     mHighlightPathBogus = false;
5078                 }
5079                 synchronized (sTempRect) {
5080                     mHighlightPath.computeBounds(sTempRect, true);
5081                     r.left = (int)sTempRect.left-1;
5082                     r.right = (int)sTempRect.right+1;
5083                 }
5084             }
5085         }
5086 
5087         // Adjust for padding and gravity.
5088         int paddingLeft = getCompoundPaddingLeft();
5089         int paddingTop = getExtendedPaddingTop();
5090         if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5091             paddingTop += getVerticalOffset(false);
5092         }
5093         r.offset(paddingLeft, paddingTop);
5094     }
5095 
5096     /**
5097      * Return the number of lines of text, or 0 if the internal Layout has not
5098      * been built.
5099      */
getLineCount()5100     public int getLineCount() {
5101         return mLayout != null ? mLayout.getLineCount() : 0;
5102     }
5103 
5104     /**
5105      * Return the baseline for the specified line (0...getLineCount() - 1)
5106      * If bounds is not null, return the top, left, right, bottom extents
5107      * of the specified line in it. If the internal Layout has not been built,
5108      * return 0 and set bounds to (0, 0, 0, 0)
5109      * @param line which line to examine (0..getLineCount() - 1)
5110      * @param bounds Optional. If not null, it returns the extent of the line
5111      * @return the Y-coordinate of the baseline
5112      */
getLineBounds(int line, Rect bounds)5113     public int getLineBounds(int line, Rect bounds) {
5114         if (mLayout == null) {
5115             if (bounds != null) {
5116                 bounds.set(0, 0, 0, 0);
5117             }
5118             return 0;
5119         }
5120         else {
5121             int baseline = mLayout.getLineBounds(line, bounds);
5122 
5123             int voffset = getExtendedPaddingTop();
5124             if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5125                 voffset += getVerticalOffset(true);
5126             }
5127             if (bounds != null) {
5128                 bounds.offset(getCompoundPaddingLeft(), voffset);
5129             }
5130             return baseline + voffset;
5131         }
5132     }
5133 
5134     @Override
getBaseline()5135     public int getBaseline() {
5136         if (mLayout == null) {
5137             return super.getBaseline();
5138         }
5139 
5140         int voffset = 0;
5141         if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5142             voffset = getVerticalOffset(true);
5143         }
5144 
5145         return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
5146     }
5147 
5148     /**
5149      * @hide
5150      * @param offsetRequired
5151      */
5152     @Override
getFadeTop(boolean offsetRequired)5153     protected int getFadeTop(boolean offsetRequired) {
5154         if (mLayout == null) return 0;
5155 
5156         int voffset = 0;
5157         if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5158             voffset = getVerticalOffset(true);
5159         }
5160 
5161         if (offsetRequired) voffset += getTopPaddingOffset();
5162 
5163         return getExtendedPaddingTop() + voffset;
5164     }
5165 
5166     /**
5167      * @hide
5168      * @param offsetRequired
5169      */
5170     @Override
getFadeHeight(boolean offsetRequired)5171     protected int getFadeHeight(boolean offsetRequired) {
5172         return mLayout != null ? mLayout.getHeight() : 0;
5173     }
5174 
5175     @Override
onKeyPreIme(int keyCode, KeyEvent event)5176     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
5177         if (keyCode == KeyEvent.KEYCODE_BACK) {
5178             boolean isInSelectionMode = mSelectionActionMode != null;
5179 
5180             if (isInSelectionMode) {
5181                 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
5182                     KeyEvent.DispatcherState state = getKeyDispatcherState();
5183                     if (state != null) {
5184                         state.startTracking(event, this);
5185                     }
5186                     return true;
5187                 } else if (event.getAction() == KeyEvent.ACTION_UP) {
5188                     KeyEvent.DispatcherState state = getKeyDispatcherState();
5189                     if (state != null) {
5190                         state.handleUpEvent(event);
5191                     }
5192                     if (event.isTracking() && !event.isCanceled()) {
5193                         if (isInSelectionMode) {
5194                             stopSelectionActionMode();
5195                             return true;
5196                         }
5197                     }
5198                 }
5199             }
5200         }
5201         return super.onKeyPreIme(keyCode, event);
5202     }
5203 
5204     @Override
onKeyDown(int keyCode, KeyEvent event)5205     public boolean onKeyDown(int keyCode, KeyEvent event) {
5206         int which = doKeyDown(keyCode, event, null);
5207         if (which == 0) {
5208             // Go through default dispatching.
5209             return super.onKeyDown(keyCode, event);
5210         }
5211 
5212         return true;
5213     }
5214 
5215     @Override
onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)5216     public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
5217         KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
5218 
5219         int which = doKeyDown(keyCode, down, event);
5220         if (which == 0) {
5221             // Go through default dispatching.
5222             return super.onKeyMultiple(keyCode, repeatCount, event);
5223         }
5224         if (which == -1) {
5225             // Consumed the whole thing.
5226             return true;
5227         }
5228 
5229         repeatCount--;
5230 
5231         // We are going to dispatch the remaining events to either the input
5232         // or movement method.  To do this, we will just send a repeated stream
5233         // of down and up events until we have done the complete repeatCount.
5234         // It would be nice if those interfaces had an onKeyMultiple() method,
5235         // but adding that is a more complicated change.
5236         KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
5237         if (which == 1) {
5238             mInput.onKeyUp(this, (Editable)mText, keyCode, up);
5239             while (--repeatCount > 0) {
5240                 mInput.onKeyDown(this, (Editable)mText, keyCode, down);
5241                 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
5242             }
5243             hideErrorIfUnchanged();
5244 
5245         } else if (which == 2) {
5246             mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
5247             while (--repeatCount > 0) {
5248                 mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
5249                 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
5250             }
5251         }
5252 
5253         return true;
5254     }
5255 
5256     /**
5257      * Returns true if pressing ENTER in this field advances focus instead
5258      * of inserting the character.  This is true mostly in single-line fields,
5259      * but also in mail addresses and subjects which will display on multiple
5260      * lines but where it doesn't make sense to insert newlines.
5261      */
shouldAdvanceFocusOnEnter()5262     private boolean shouldAdvanceFocusOnEnter() {
5263         if (mInput == null) {
5264             return false;
5265         }
5266 
5267         if (mSingleLine) {
5268             return true;
5269         }
5270 
5271         if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
5272             int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
5273             if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
5274                     || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
5275                 return true;
5276             }
5277         }
5278 
5279         return false;
5280     }
5281 
5282     /**
5283      * Returns true if pressing TAB in this field advances focus instead
5284      * of inserting the character.  Insert tabs only in multi-line editors.
5285      */
shouldAdvanceFocusOnTab()5286     private boolean shouldAdvanceFocusOnTab() {
5287         if (mInput != null && !mSingleLine) {
5288             if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
5289                 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
5290                 if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
5291                         || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
5292                     return false;
5293                 }
5294             }
5295         }
5296         return true;
5297     }
5298 
doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent)5299     private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
5300         if (!isEnabled()) {
5301             return 0;
5302         }
5303 
5304         switch (keyCode) {
5305             case KeyEvent.KEYCODE_ENTER:
5306                 mEnterKeyIsDown = true;
5307                 if (event.hasNoModifiers()) {
5308                     // When mInputContentType is set, we know that we are
5309                     // running in a "modern" cupcake environment, so don't need
5310                     // to worry about the application trying to capture
5311                     // enter key events.
5312                     if (mInputContentType != null) {
5313                         // If there is an action listener, given them a
5314                         // chance to consume the event.
5315                         if (mInputContentType.onEditorActionListener != null &&
5316                                 mInputContentType.onEditorActionListener.onEditorAction(
5317                                 this, EditorInfo.IME_NULL, event)) {
5318                             mInputContentType.enterDown = true;
5319                             // We are consuming the enter key for them.
5320                             return -1;
5321                         }
5322                     }
5323 
5324                     // If our editor should move focus when enter is pressed, or
5325                     // this is a generated event from an IME action button, then
5326                     // don't let it be inserted into the text.
5327                     if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
5328                             || shouldAdvanceFocusOnEnter()) {
5329                         if (mOnClickListener != null) {
5330                             return 0;
5331                         }
5332                         return -1;
5333                     }
5334                 }
5335                 break;
5336 
5337             case KeyEvent.KEYCODE_DPAD_CENTER:
5338                 mDPadCenterIsDown = true;
5339                 if (event.hasNoModifiers()) {
5340                     if (shouldAdvanceFocusOnEnter()) {
5341                         return 0;
5342                     }
5343                 }
5344                 break;
5345 
5346             case KeyEvent.KEYCODE_TAB:
5347                 if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
5348                     if (shouldAdvanceFocusOnTab()) {
5349                         return 0;
5350                     }
5351                 }
5352                 break;
5353 
5354                 // Has to be done on key down (and not on key up) to correctly be intercepted.
5355             case KeyEvent.KEYCODE_BACK:
5356                 if (mSelectionActionMode != null) {
5357                     stopSelectionActionMode();
5358                     return -1;
5359                 }
5360                 break;
5361         }
5362 
5363         if (mInput != null) {
5364             resetErrorChangedFlag();
5365 
5366             boolean doDown = true;
5367             if (otherEvent != null) {
5368                 try {
5369                     beginBatchEdit();
5370                     final boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent);
5371                     hideErrorIfUnchanged();
5372                     doDown = false;
5373                     if (handled) {
5374                         return -1;
5375                     }
5376                 } catch (AbstractMethodError e) {
5377                     // onKeyOther was added after 1.0, so if it isn't
5378                     // implemented we need to try to dispatch as a regular down.
5379                 } finally {
5380                     endBatchEdit();
5381                 }
5382             }
5383 
5384             if (doDown) {
5385                 beginBatchEdit();
5386                 final boolean handled = mInput.onKeyDown(this, (Editable) mText, keyCode, event);
5387                 endBatchEdit();
5388                 hideErrorIfUnchanged();
5389                 if (handled) return 1;
5390             }
5391         }
5392 
5393         // bug 650865: sometimes we get a key event before a layout.
5394         // don't try to move around if we don't know the layout.
5395 
5396         if (mMovement != null && mLayout != null) {
5397             boolean doDown = true;
5398             if (otherEvent != null) {
5399                 try {
5400                     boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
5401                             otherEvent);
5402                     doDown = false;
5403                     if (handled) {
5404                         return -1;
5405                     }
5406                 } catch (AbstractMethodError e) {
5407                     // onKeyOther was added after 1.0, so if it isn't
5408                     // implemented we need to try to dispatch as a regular down.
5409                 }
5410             }
5411             if (doDown) {
5412                 if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
5413                     return 2;
5414             }
5415         }
5416 
5417         return 0;
5418     }
5419 
5420     /**
5421      * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
5422      * can be recorded.
5423      * @hide
5424      */
resetErrorChangedFlag()5425     public void resetErrorChangedFlag() {
5426         /*
5427          * Keep track of what the error was before doing the input
5428          * so that if an input filter changed the error, we leave
5429          * that error showing.  Otherwise, we take down whatever
5430          * error was showing when the user types something.
5431          */
5432         mErrorWasChanged = false;
5433     }
5434 
5435     /**
5436      * @hide
5437      */
hideErrorIfUnchanged()5438     public void hideErrorIfUnchanged() {
5439         if (mError != null && !mErrorWasChanged) {
5440             setError(null, null);
5441         }
5442     }
5443 
5444     @Override
onKeyUp(int keyCode, KeyEvent event)5445     public boolean onKeyUp(int keyCode, KeyEvent event) {
5446         if (!isEnabled()) {
5447             return super.onKeyUp(keyCode, event);
5448         }
5449 
5450         switch (keyCode) {
5451             case KeyEvent.KEYCODE_DPAD_CENTER:
5452                 mDPadCenterIsDown = false;
5453                 if (event.hasNoModifiers()) {
5454                     /*
5455                      * If there is a click listener, just call through to
5456                      * super, which will invoke it.
5457                      *
5458                      * If there isn't a click listener, try to show the soft
5459                      * input method.  (It will also
5460                      * call performClick(), but that won't do anything in
5461                      * this case.)
5462                      */
5463                     if (mOnClickListener == null) {
5464                         if (mMovement != null && mText instanceof Editable
5465                                 && mLayout != null && onCheckIsTextEditor()) {
5466                             InputMethodManager imm = InputMethodManager.peekInstance();
5467                             viewClicked(imm);
5468                             if (imm != null) {
5469                                 imm.showSoftInput(this, 0);
5470                             }
5471                         }
5472                     }
5473                 }
5474                 return super.onKeyUp(keyCode, event);
5475 
5476             case KeyEvent.KEYCODE_ENTER:
5477                 mEnterKeyIsDown = false;
5478                 if (event.hasNoModifiers()) {
5479                     if (mInputContentType != null
5480                             && mInputContentType.onEditorActionListener != null
5481                             && mInputContentType.enterDown) {
5482                         mInputContentType.enterDown = false;
5483                         if (mInputContentType.onEditorActionListener.onEditorAction(
5484                                 this, EditorInfo.IME_NULL, event)) {
5485                             return true;
5486                         }
5487                     }
5488 
5489                     if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
5490                             || shouldAdvanceFocusOnEnter()) {
5491                         /*
5492                          * If there is a click listener, just call through to
5493                          * super, which will invoke it.
5494                          *
5495                          * If there isn't a click listener, try to advance focus,
5496                          * but still call through to super, which will reset the
5497                          * pressed state and longpress state.  (It will also
5498                          * call performClick(), but that won't do anything in
5499                          * this case.)
5500                          */
5501                         if (mOnClickListener == null) {
5502                             View v = focusSearch(FOCUS_DOWN);
5503 
5504                             if (v != null) {
5505                                 if (!v.requestFocus(FOCUS_DOWN)) {
5506                                     throw new IllegalStateException(
5507                                             "focus search returned a view " +
5508                                             "that wasn't able to take focus!");
5509                                 }
5510 
5511                                 /*
5512                                  * Return true because we handled the key; super
5513                                  * will return false because there was no click
5514                                  * listener.
5515                                  */
5516                                 super.onKeyUp(keyCode, event);
5517                                 return true;
5518                             } else if ((event.getFlags()
5519                                     & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
5520                                 // No target for next focus, but make sure the IME
5521                                 // if this came from it.
5522                                 InputMethodManager imm = InputMethodManager.peekInstance();
5523                                 if (imm != null && imm.isActive(this)) {
5524                                     imm.hideSoftInputFromWindow(getWindowToken(), 0);
5525                                 }
5526                             }
5527                         }
5528                     }
5529                     return super.onKeyUp(keyCode, event);
5530                 }
5531                 break;
5532         }
5533 
5534         if (mInput != null)
5535             if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
5536                 return true;
5537 
5538         if (mMovement != null && mLayout != null)
5539             if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
5540                 return true;
5541 
5542         return super.onKeyUp(keyCode, event);
5543     }
5544 
onCheckIsTextEditor()5545     @Override public boolean onCheckIsTextEditor() {
5546         return mInputType != EditorInfo.TYPE_NULL;
5547     }
5548 
onCreateInputConnection(EditorInfo outAttrs)5549     @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
5550         if (onCheckIsTextEditor() && isEnabled()) {
5551             if (mInputMethodState == null) {
5552                 mInputMethodState = new InputMethodState();
5553             }
5554             outAttrs.inputType = mInputType;
5555             if (mInputContentType != null) {
5556                 outAttrs.imeOptions = mInputContentType.imeOptions;
5557                 outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
5558                 outAttrs.actionLabel = mInputContentType.imeActionLabel;
5559                 outAttrs.actionId = mInputContentType.imeActionId;
5560                 outAttrs.extras = mInputContentType.extras;
5561             } else {
5562                 outAttrs.imeOptions = EditorInfo.IME_NULL;
5563             }
5564             if (focusSearch(FOCUS_DOWN) != null) {
5565                 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
5566             }
5567             if (focusSearch(FOCUS_UP) != null) {
5568                 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
5569             }
5570             if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
5571                     == EditorInfo.IME_ACTION_UNSPECIFIED) {
5572                 if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
5573                     // An action has not been set, but the enter key will move to
5574                     // the next focus, so set the action to that.
5575                     outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
5576                 } else {
5577                     // An action has not been set, and there is no focus to move
5578                     // to, so let's just supply a "done" action.
5579                     outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
5580                 }
5581                 if (!shouldAdvanceFocusOnEnter()) {
5582                     outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5583                 }
5584             }
5585             if (isMultilineInputType(outAttrs.inputType)) {
5586                 // Multi-line text editors should always show an enter key.
5587                 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5588             }
5589             outAttrs.hintText = mHint;
5590             if (mText instanceof Editable) {
5591                 InputConnection ic = new EditableInputConnection(this);
5592                 outAttrs.initialSelStart = getSelectionStart();
5593                 outAttrs.initialSelEnd = getSelectionEnd();
5594                 outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
5595                 return ic;
5596             }
5597         }
5598         return null;
5599     }
5600 
5601     /**
5602      * If this TextView contains editable content, extract a portion of it
5603      * based on the information in <var>request</var> in to <var>outText</var>.
5604      * @return Returns true if the text was successfully extracted, else false.
5605      */
extractText(ExtractedTextRequest request, ExtractedText outText)5606     public boolean extractText(ExtractedTextRequest request,
5607             ExtractedText outText) {
5608         return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
5609                 EXTRACT_UNKNOWN, outText);
5610     }
5611 
5612     static final int EXTRACT_NOTHING = -2;
5613     static final int EXTRACT_UNKNOWN = -1;
5614 
extractTextInternal(ExtractedTextRequest request, int partialStartOffset, int partialEndOffset, int delta, ExtractedText outText)5615     boolean extractTextInternal(ExtractedTextRequest request,
5616             int partialStartOffset, int partialEndOffset, int delta,
5617             ExtractedText outText) {
5618         final CharSequence content = mText;
5619         if (content != null) {
5620             if (partialStartOffset != EXTRACT_NOTHING) {
5621                 final int N = content.length();
5622                 if (partialStartOffset < 0) {
5623                     outText.partialStartOffset = outText.partialEndOffset = -1;
5624                     partialStartOffset = 0;
5625                     partialEndOffset = N;
5626                 } else {
5627                     // Now use the delta to determine the actual amount of text
5628                     // we need.
5629                     partialEndOffset += delta;
5630                     // Adjust offsets to ensure we contain full spans.
5631                     if (content instanceof Spanned) {
5632                         Spanned spanned = (Spanned)content;
5633                         Object[] spans = spanned.getSpans(partialStartOffset,
5634                                 partialEndOffset, ParcelableSpan.class);
5635                         int i = spans.length;
5636                         while (i > 0) {
5637                             i--;
5638                             int j = spanned.getSpanStart(spans[i]);
5639                             if (j < partialStartOffset) partialStartOffset = j;
5640                             j = spanned.getSpanEnd(spans[i]);
5641                             if (j > partialEndOffset) partialEndOffset = j;
5642                         }
5643                     }
5644                     outText.partialStartOffset = partialStartOffset;
5645                     outText.partialEndOffset = partialEndOffset - delta;
5646 
5647                     if (partialStartOffset > N) {
5648                         partialStartOffset = N;
5649                     } else if (partialStartOffset < 0) {
5650                         partialStartOffset = 0;
5651                     }
5652                     if (partialEndOffset > N) {
5653                         partialEndOffset = N;
5654                     } else if (partialEndOffset < 0) {
5655                         partialEndOffset = 0;
5656                     }
5657                 }
5658                 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
5659                     outText.text = content.subSequence(partialStartOffset,
5660                             partialEndOffset);
5661                 } else {
5662                     outText.text = TextUtils.substring(content, partialStartOffset,
5663                             partialEndOffset);
5664                 }
5665             } else {
5666                 outText.partialStartOffset = 0;
5667                 outText.partialEndOffset = 0;
5668                 outText.text = "";
5669             }
5670             outText.flags = 0;
5671             if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
5672                 outText.flags |= ExtractedText.FLAG_SELECTING;
5673             }
5674             if (mSingleLine) {
5675                 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
5676             }
5677             outText.startOffset = 0;
5678             outText.selectionStart = getSelectionStart();
5679             outText.selectionEnd = getSelectionEnd();
5680             return true;
5681         }
5682         return false;
5683     }
5684 
reportExtractedText()5685     boolean reportExtractedText() {
5686         final InputMethodState ims = mInputMethodState;
5687         if (ims != null) {
5688             final boolean contentChanged = ims.mContentChanged;
5689             if (contentChanged || ims.mSelectionModeChanged) {
5690                 ims.mContentChanged = false;
5691                 ims.mSelectionModeChanged = false;
5692                 final ExtractedTextRequest req = mInputMethodState.mExtracting;
5693                 if (req != null) {
5694                     InputMethodManager imm = InputMethodManager.peekInstance();
5695                     if (imm != null) {
5696                         if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
5697                                 + ims.mChangedStart + " end=" + ims.mChangedEnd
5698                                 + " delta=" + ims.mChangedDelta);
5699                         if (ims.mChangedStart < 0 && !contentChanged) {
5700                             ims.mChangedStart = EXTRACT_NOTHING;
5701                         }
5702                         if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
5703                                 ims.mChangedDelta, ims.mTmpExtracted)) {
5704                             if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
5705                                     + ims.mTmpExtracted.partialStartOffset
5706                                     + " end=" + ims.mTmpExtracted.partialEndOffset
5707                                     + ": " + ims.mTmpExtracted.text);
5708                             imm.updateExtractedText(this, req.token,
5709                                     mInputMethodState.mTmpExtracted);
5710                             ims.mChangedStart = EXTRACT_UNKNOWN;
5711                             ims.mChangedEnd = EXTRACT_UNKNOWN;
5712                             ims.mChangedDelta = 0;
5713                             ims.mContentChanged = false;
5714                             return true;
5715                         }
5716                     }
5717                 }
5718             }
5719         }
5720         return false;
5721     }
5722 
5723     /**
5724      * This is used to remove all style-impacting spans from text before new
5725      * extracted text is being replaced into it, so that we don't have any
5726      * lingering spans applied during the replace.
5727      */
removeParcelableSpans(Spannable spannable, int start, int end)5728     static void removeParcelableSpans(Spannable spannable, int start, int end) {
5729         Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
5730         int i = spans.length;
5731         while (i > 0) {
5732             i--;
5733             spannable.removeSpan(spans[i]);
5734         }
5735     }
5736 
5737     /**
5738      * Apply to this text view the given extracted text, as previously
5739      * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
5740      */
setExtractedText(ExtractedText text)5741     public void setExtractedText(ExtractedText text) {
5742         Editable content = getEditableText();
5743         if (text.text != null) {
5744             if (content == null) {
5745                 setText(text.text, TextView.BufferType.EDITABLE);
5746             } else if (text.partialStartOffset < 0) {
5747                 removeParcelableSpans(content, 0, content.length());
5748                 content.replace(0, content.length(), text.text);
5749             } else {
5750                 final int N = content.length();
5751                 int start = text.partialStartOffset;
5752                 if (start > N) start = N;
5753                 int end = text.partialEndOffset;
5754                 if (end > N) end = N;
5755                 removeParcelableSpans(content, start, end);
5756                 content.replace(start, end, text.text);
5757             }
5758         }
5759 
5760         // Now set the selection position...  make sure it is in range, to
5761         // avoid crashes.  If this is a partial update, it is possible that
5762         // the underlying text may have changed, causing us problems here.
5763         // Also we just don't want to trust clients to do the right thing.
5764         Spannable sp = (Spannable)getText();
5765         final int N = sp.length();
5766         int start = text.selectionStart;
5767         if (start < 0) start = 0;
5768         else if (start > N) start = N;
5769         int end = text.selectionEnd;
5770         if (end < 0) end = 0;
5771         else if (end > N) end = N;
5772         Selection.setSelection(sp, start, end);
5773 
5774         // Finally, update the selection mode.
5775         if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
5776             MetaKeyKeyListener.startSelecting(this, sp);
5777         } else {
5778             MetaKeyKeyListener.stopSelecting(this, sp);
5779         }
5780     }
5781 
5782     /**
5783      * @hide
5784      */
setExtracting(ExtractedTextRequest req)5785     public void setExtracting(ExtractedTextRequest req) {
5786         if (mInputMethodState != null) {
5787             mInputMethodState.mExtracting = req;
5788         }
5789         // This would stop a possible selection mode, but no such mode is started in case
5790         // extracted mode will start. Some text is selected though, and will trigger an action mode
5791         // in the extracted view.
5792         hideControllers();
5793     }
5794 
5795     /**
5796      * Called by the framework in response to a text completion from
5797      * the current input method, provided by it calling
5798      * {@link InputConnection#commitCompletion
5799      * InputConnection.commitCompletion()}.  The default implementation does
5800      * nothing; text views that are supporting auto-completion should override
5801      * this to do their desired behavior.
5802      *
5803      * @param text The auto complete text the user has selected.
5804      */
onCommitCompletion(CompletionInfo text)5805     public void onCommitCompletion(CompletionInfo text) {
5806         // intentionally empty
5807     }
5808 
5809     /**
5810      * Called by the framework in response to a text auto-correction (such as fixing a typo using a
5811      * a dictionnary) from the current input method, provided by it calling
5812      * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
5813      * implementation flashes the background of the corrected word to provide feedback to the user.
5814      *
5815      * @param info The auto correct info about the text that was corrected.
5816      */
onCommitCorrection(CorrectionInfo info)5817     public void onCommitCorrection(CorrectionInfo info) {
5818         if (mCorrectionHighlighter == null) {
5819             mCorrectionHighlighter = new CorrectionHighlighter();
5820         } else {
5821             mCorrectionHighlighter.invalidate(false);
5822         }
5823 
5824         mCorrectionHighlighter.highlight(info);
5825     }
5826 
5827     private class CorrectionHighlighter {
5828         private final Path mPath = new Path();
5829         private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5830         private int mStart, mEnd;
5831         private long mFadingStartTime;
5832         private final static int FADE_OUT_DURATION = 400;
5833 
CorrectionHighlighter()5834         public CorrectionHighlighter() {
5835             mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
5836             mPaint.setStyle(Paint.Style.FILL);
5837         }
5838 
highlight(CorrectionInfo info)5839         public void highlight(CorrectionInfo info) {
5840             mStart = info.getOffset();
5841             mEnd = mStart + info.getNewText().length();
5842             mFadingStartTime = SystemClock.uptimeMillis();
5843 
5844             if (mStart < 0 || mEnd < 0) {
5845                 stopAnimation();
5846             }
5847         }
5848 
draw(Canvas canvas, int cursorOffsetVertical)5849         public void draw(Canvas canvas, int cursorOffsetVertical) {
5850             if (updatePath() && updatePaint()) {
5851                 if (cursorOffsetVertical != 0) {
5852                     canvas.translate(0, cursorOffsetVertical);
5853                 }
5854 
5855                 canvas.drawPath(mPath, mPaint);
5856 
5857                 if (cursorOffsetVertical != 0) {
5858                     canvas.translate(0, -cursorOffsetVertical);
5859                 }
5860                 invalidate(true);
5861             } else {
5862                 stopAnimation();
5863                 invalidate(false);
5864             }
5865         }
5866 
updatePaint()5867         private boolean updatePaint() {
5868             final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5869             if (duration > FADE_OUT_DURATION) return false;
5870 
5871             final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5872             final int highlightColorAlpha = Color.alpha(mHighlightColor);
5873             final int color = (mHighlightColor & 0x00FFFFFF) +
5874                     ((int) (highlightColorAlpha * coef) << 24);
5875             mPaint.setColor(color);
5876             return true;
5877         }
5878 
updatePath()5879         private boolean updatePath() {
5880             final Layout layout = TextView.this.mLayout;
5881             if (layout == null) return false;
5882 
5883             // Update in case text is edited while the animation is run
5884             final int length = mText.length();
5885             int start = Math.min(length, mStart);
5886             int end = Math.min(length, mEnd);
5887 
5888             mPath.reset();
5889             TextView.this.mLayout.getSelectionPath(start, end, mPath);
5890             return true;
5891         }
5892 
invalidate(boolean delayed)5893         private void invalidate(boolean delayed) {
5894             if (TextView.this.mLayout == null) return;
5895 
5896             synchronized (sTempRect) {
5897                 mPath.computeBounds(sTempRect, false);
5898 
5899                 int left = getCompoundPaddingLeft();
5900                 int top = getExtendedPaddingTop() + getVerticalOffset(true);
5901 
5902                 if (delayed) {
5903                     TextView.this.postInvalidateDelayed(16, // 60 Hz update
5904                             left + (int) sTempRect.left, top + (int) sTempRect.top,
5905                             left + (int) sTempRect.right, top + (int) sTempRect.bottom);
5906                 } else {
5907                     TextView.this.postInvalidate((int) sTempRect.left, (int) sTempRect.top,
5908                             (int) sTempRect.right, (int) sTempRect.bottom);
5909                 }
5910             }
5911         }
5912 
stopAnimation()5913         private void stopAnimation() {
5914             TextView.this.mCorrectionHighlighter = null;
5915         }
5916     }
5917 
beginBatchEdit()5918     public void beginBatchEdit() {
5919         mInBatchEditControllers = true;
5920         final InputMethodState ims = mInputMethodState;
5921         if (ims != null) {
5922             int nesting = ++ims.mBatchEditNesting;
5923             if (nesting == 1) {
5924                 ims.mCursorChanged = false;
5925                 ims.mChangedDelta = 0;
5926                 if (ims.mContentChanged) {
5927                     // We already have a pending change from somewhere else,
5928                     // so turn this into a full update.
5929                     ims.mChangedStart = 0;
5930                     ims.mChangedEnd = mText.length();
5931                 } else {
5932                     ims.mChangedStart = EXTRACT_UNKNOWN;
5933                     ims.mChangedEnd = EXTRACT_UNKNOWN;
5934                     ims.mContentChanged = false;
5935                 }
5936                 onBeginBatchEdit();
5937             }
5938         }
5939     }
5940 
endBatchEdit()5941     public void endBatchEdit() {
5942         mInBatchEditControllers = false;
5943         final InputMethodState ims = mInputMethodState;
5944         if (ims != null) {
5945             int nesting = --ims.mBatchEditNesting;
5946             if (nesting == 0) {
5947                 finishBatchEdit(ims);
5948             }
5949         }
5950     }
5951 
ensureEndedBatchEdit()5952     void ensureEndedBatchEdit() {
5953         final InputMethodState ims = mInputMethodState;
5954         if (ims != null && ims.mBatchEditNesting != 0) {
5955             ims.mBatchEditNesting = 0;
5956             finishBatchEdit(ims);
5957         }
5958     }
5959 
finishBatchEdit(final InputMethodState ims)5960     void finishBatchEdit(final InputMethodState ims) {
5961         onEndBatchEdit();
5962 
5963         if (ims.mContentChanged || ims.mSelectionModeChanged) {
5964             updateAfterEdit();
5965             reportExtractedText();
5966         } else if (ims.mCursorChanged) {
5967             // Cheezy way to get us to report the current cursor location.
5968             invalidateCursor();
5969         }
5970     }
5971 
updateAfterEdit()5972     void updateAfterEdit() {
5973         invalidate();
5974         int curs = getSelectionStart();
5975 
5976         if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
5977             registerForPreDraw();
5978         }
5979 
5980         if (curs >= 0) {
5981             mHighlightPathBogus = true;
5982             makeBlink();
5983             bringPointIntoView(curs);
5984         }
5985 
5986         checkForResize();
5987     }
5988 
5989     /**
5990      * Called by the framework in response to a request to begin a batch
5991      * of edit operations through a call to link {@link #beginBatchEdit()}.
5992      */
onBeginBatchEdit()5993     public void onBeginBatchEdit() {
5994         // intentionally empty
5995     }
5996 
5997     /**
5998      * Called by the framework in response to a request to end a batch
5999      * of edit operations through a call to link {@link #endBatchEdit}.
6000      */
onEndBatchEdit()6001     public void onEndBatchEdit() {
6002         // intentionally empty
6003     }
6004 
6005     /**
6006      * Called by the framework in response to a private command from the
6007      * current method, provided by it calling
6008      * {@link InputConnection#performPrivateCommand
6009      * InputConnection.performPrivateCommand()}.
6010      *
6011      * @param action The action name of the command.
6012      * @param data Any additional data for the command.  This may be null.
6013      * @return Return true if you handled the command, else false.
6014      */
onPrivateIMECommand(String action, Bundle data)6015     public boolean onPrivateIMECommand(String action, Bundle data) {
6016         return false;
6017     }
6018 
nullLayouts()6019     private void nullLayouts() {
6020         if (mLayout instanceof BoringLayout && mSavedLayout == null) {
6021             mSavedLayout = (BoringLayout) mLayout;
6022         }
6023         if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
6024             mSavedHintLayout = (BoringLayout) mHintLayout;
6025         }
6026 
6027         mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
6028 
6029         // Since it depends on the value of mLayout
6030         prepareCursorControllers();
6031     }
6032 
6033     /**
6034      * Make a new Layout based on the already-measured size of the view,
6035      * on the assumption that it was measured correctly at some point.
6036      */
assumeLayout()6037     private void assumeLayout() {
6038         int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6039 
6040         if (width < 1) {
6041             width = 0;
6042         }
6043 
6044         int physicalWidth = width;
6045 
6046         if (mHorizontallyScrolling) {
6047             width = VERY_WIDE;
6048         }
6049 
6050         makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
6051                       physicalWidth, false);
6052     }
6053 
6054     @Override
resetResolvedLayoutDirection()6055     protected void resetResolvedLayoutDirection() {
6056         super.resetResolvedLayoutDirection();
6057 
6058         if (mLayoutAlignment != null &&
6059                 (mTextAlign == TextAlign.VIEW_START ||
6060                 mTextAlign == TextAlign.VIEW_END)) {
6061             mLayoutAlignment = null;
6062         }
6063     }
6064 
getLayoutAlignment()6065     private Layout.Alignment getLayoutAlignment() {
6066         if (mLayoutAlignment == null) {
6067             Layout.Alignment alignment;
6068             TextAlign textAlign = mTextAlign;
6069             switch (textAlign) {
6070                 case INHERIT:
6071                     // fall through to gravity temporarily
6072                     // intention is to inherit value through view hierarchy.
6073                 case GRAVITY:
6074                     switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
6075                         case Gravity.START:
6076                             alignment = Layout.Alignment.ALIGN_NORMAL;
6077                             break;
6078                         case Gravity.END:
6079                             alignment = Layout.Alignment.ALIGN_OPPOSITE;
6080                             break;
6081                         case Gravity.LEFT:
6082                             alignment = Layout.Alignment.ALIGN_LEFT;
6083                             break;
6084                         case Gravity.RIGHT:
6085                             alignment = Layout.Alignment.ALIGN_RIGHT;
6086                             break;
6087                         case Gravity.CENTER_HORIZONTAL:
6088                             alignment = Layout.Alignment.ALIGN_CENTER;
6089                             break;
6090                         default:
6091                             alignment = Layout.Alignment.ALIGN_NORMAL;
6092                             break;
6093                     }
6094                     break;
6095                 case TEXT_START:
6096                     alignment = Layout.Alignment.ALIGN_NORMAL;
6097                     break;
6098                 case TEXT_END:
6099                     alignment = Layout.Alignment.ALIGN_OPPOSITE;
6100                     break;
6101                 case CENTER:
6102                     alignment = Layout.Alignment.ALIGN_CENTER;
6103                     break;
6104                 case VIEW_START:
6105                     alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
6106                             Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
6107                     break;
6108                 case VIEW_END:
6109                     alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
6110                             Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
6111                     break;
6112                 default:
6113                     alignment = Layout.Alignment.ALIGN_NORMAL;
6114                     break;
6115             }
6116             mLayoutAlignment = alignment;
6117         }
6118         return mLayoutAlignment;
6119     }
6120 
6121     /**
6122      * The width passed in is now the desired layout width,
6123      * not the full view width with padding.
6124      * {@hide}
6125      */
makeNewLayout(int wantWidth, int hintWidth, BoringLayout.Metrics boring, BoringLayout.Metrics hintBoring, int ellipsisWidth, boolean bringIntoView)6126     protected void makeNewLayout(int wantWidth, int hintWidth,
6127                                  BoringLayout.Metrics boring,
6128                                  BoringLayout.Metrics hintBoring,
6129                                  int ellipsisWidth, boolean bringIntoView) {
6130         stopMarquee();
6131 
6132         // Update "old" cached values
6133         mOldMaximum = mMaximum;
6134         mOldMaxMode = mMaxMode;
6135 
6136         mHighlightPathBogus = true;
6137 
6138         if (wantWidth < 0) {
6139             wantWidth = 0;
6140         }
6141         if (hintWidth < 0) {
6142             hintWidth = 0;
6143         }
6144 
6145         Layout.Alignment alignment = getLayoutAlignment();
6146         boolean shouldEllipsize = mEllipsize != null && mInput == null;
6147         final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
6148                 mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
6149         TruncateAt effectiveEllipsize = mEllipsize;
6150         if (mEllipsize == TruncateAt.MARQUEE &&
6151                 mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
6152             effectiveEllipsize = TruncateAt.END_SMALL;
6153         }
6154 
6155         if (mTextDir == null) {
6156             resolveTextDirection();
6157         }
6158 
6159         mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
6160                 effectiveEllipsize, effectiveEllipsize == mEllipsize);
6161         if (switchEllipsize) {
6162             TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
6163                     TruncateAt.END : TruncateAt.MARQUEE;
6164             mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
6165                     shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
6166         }
6167 
6168         shouldEllipsize = mEllipsize != null;
6169         mHintLayout = null;
6170 
6171         if (mHint != null) {
6172             if (shouldEllipsize) hintWidth = wantWidth;
6173 
6174             if (hintBoring == UNKNOWN_BORING) {
6175                 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
6176                                                    mHintBoring);
6177                 if (hintBoring != null) {
6178                     mHintBoring = hintBoring;
6179                 }
6180             }
6181 
6182             if (hintBoring != null) {
6183                 if (hintBoring.width <= hintWidth &&
6184                     (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
6185                     if (mSavedHintLayout != null) {
6186                         mHintLayout = mSavedHintLayout.
6187                                 replaceOrMake(mHint, mTextPaint,
6188                                 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6189                                 hintBoring, mIncludePad);
6190                     } else {
6191                         mHintLayout = BoringLayout.make(mHint, mTextPaint,
6192                                 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6193                                 hintBoring, mIncludePad);
6194                     }
6195 
6196                     mSavedHintLayout = (BoringLayout) mHintLayout;
6197                 } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
6198                     if (mSavedHintLayout != null) {
6199                         mHintLayout = mSavedHintLayout.
6200                                 replaceOrMake(mHint, mTextPaint,
6201                                 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6202                                 hintBoring, mIncludePad, mEllipsize,
6203                                 ellipsisWidth);
6204                     } else {
6205                         mHintLayout = BoringLayout.make(mHint, mTextPaint,
6206                                 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6207                                 hintBoring, mIncludePad, mEllipsize,
6208                                 ellipsisWidth);
6209                     }
6210                 } else if (shouldEllipsize) {
6211                     mHintLayout = new StaticLayout(mHint,
6212                                 0, mHint.length(),
6213                                 mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
6214                                 mSpacingAdd, mIncludePad, mEllipsize,
6215                                 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6216                 } else {
6217                     mHintLayout = new StaticLayout(mHint, mTextPaint,
6218                             hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
6219                             mIncludePad);
6220                 }
6221             } else if (shouldEllipsize) {
6222                 mHintLayout = new StaticLayout(mHint,
6223                             0, mHint.length(),
6224                             mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
6225                             mSpacingAdd, mIncludePad, mEllipsize,
6226                             ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6227             } else {
6228                 mHintLayout = new StaticLayout(mHint, mTextPaint,
6229                         hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
6230                         mIncludePad);
6231             }
6232         }
6233 
6234         if (bringIntoView) {
6235             registerForPreDraw();
6236         }
6237 
6238         if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
6239             if (!compressText(ellipsisWidth)) {
6240                 final int height = mLayoutParams.height;
6241                 // If the size of the view does not depend on the size of the text, try to
6242                 // start the marquee immediately
6243                 if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
6244                     startMarquee();
6245                 } else {
6246                     // Defer the start of the marquee until we know our width (see setFrame())
6247                     mRestartMarquee = true;
6248                 }
6249             }
6250         }
6251 
6252         // CursorControllers need a non-null mLayout
6253         prepareCursorControllers();
6254     }
6255 
makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize, boolean useSaved)6256     private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
6257             Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
6258             boolean useSaved) {
6259         Layout result = null;
6260         if (mText instanceof Spannable) {
6261             result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
6262                     alignment, mTextDir, mSpacingMult,
6263                     mSpacingAdd, mIncludePad, mInput == null ? effectiveEllipsize : null,
6264                             ellipsisWidth);
6265         } else {
6266             if (boring == UNKNOWN_BORING) {
6267                 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
6268                 if (boring != null) {
6269                     mBoring = boring;
6270                 }
6271             }
6272 
6273             if (boring != null) {
6274                 if (boring.width <= wantWidth &&
6275                         (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
6276                     if (useSaved && mSavedLayout != null) {
6277                         result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
6278                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
6279                                 boring, mIncludePad);
6280                     } else {
6281                         result = BoringLayout.make(mTransformed, mTextPaint,
6282                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
6283                                 boring, mIncludePad);
6284                     }
6285 
6286                     if (useSaved) {
6287                         mSavedLayout = (BoringLayout) result;
6288                     }
6289                 } else if (shouldEllipsize && boring.width <= wantWidth) {
6290                     if (useSaved && mSavedLayout != null) {
6291                         result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
6292                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
6293                                 boring, mIncludePad, effectiveEllipsize,
6294                                 ellipsisWidth);
6295                     } else {
6296                         result = BoringLayout.make(mTransformed, mTextPaint,
6297                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
6298                                 boring, mIncludePad, effectiveEllipsize,
6299                                 ellipsisWidth);
6300                     }
6301                 } else if (shouldEllipsize) {
6302                     result = new StaticLayout(mTransformed,
6303                             0, mTransformed.length(),
6304                             mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
6305                             mSpacingAdd, mIncludePad, effectiveEllipsize,
6306                             ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6307                 } else {
6308                     result = new StaticLayout(mTransformed, mTextPaint,
6309                             wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
6310                             mIncludePad);
6311                 }
6312             } else if (shouldEllipsize) {
6313                 result = new StaticLayout(mTransformed,
6314                         0, mTransformed.length(),
6315                         mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
6316                         mSpacingAdd, mIncludePad, effectiveEllipsize,
6317                         ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6318             } else {
6319                 result = new StaticLayout(mTransformed, mTextPaint,
6320                         wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
6321                         mIncludePad);
6322             }
6323         }
6324         return result;
6325     }
6326 
compressText(float width)6327     private boolean compressText(float width) {
6328         if (isHardwareAccelerated()) return false;
6329 
6330         // Only compress the text if it hasn't been compressed by the previous pass
6331         if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
6332                 mTextPaint.getTextScaleX() == 1.0f) {
6333             final float textWidth = mLayout.getLineWidth(0);
6334             final float overflow = (textWidth + 1.0f - width) / width;
6335             if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
6336                 mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
6337                 post(new Runnable() {
6338                     public void run() {
6339                         requestLayout();
6340                     }
6341                 });
6342                 return true;
6343             }
6344         }
6345 
6346         return false;
6347     }
6348 
desired(Layout layout)6349     private static int desired(Layout layout) {
6350         int n = layout.getLineCount();
6351         CharSequence text = layout.getText();
6352         float max = 0;
6353 
6354         // if any line was wrapped, we can't use it.
6355         // but it's ok for the last line not to have a newline
6356 
6357         for (int i = 0; i < n - 1; i++) {
6358             if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
6359                 return -1;
6360         }
6361 
6362         for (int i = 0; i < n; i++) {
6363             max = Math.max(max, layout.getLineWidth(i));
6364         }
6365 
6366         return (int) FloatMath.ceil(max);
6367     }
6368 
6369     /**
6370      * Set whether the TextView includes extra top and bottom padding to make
6371      * room for accents that go above the normal ascent and descent.
6372      * The default is true.
6373      *
6374      * @attr ref android.R.styleable#TextView_includeFontPadding
6375      */
setIncludeFontPadding(boolean includepad)6376     public void setIncludeFontPadding(boolean includepad) {
6377         if (mIncludePad != includepad) {
6378             mIncludePad = includepad;
6379 
6380             if (mLayout != null) {
6381                 nullLayouts();
6382                 requestLayout();
6383                 invalidate();
6384             }
6385         }
6386     }
6387 
6388     private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
6389 
6390     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)6391     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
6392         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
6393         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
6394         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
6395         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
6396 
6397         int width;
6398         int height;
6399 
6400         BoringLayout.Metrics boring = UNKNOWN_BORING;
6401         BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
6402 
6403         if (mTextDir == null) {
6404             resolveTextDirection();
6405         }
6406 
6407         int des = -1;
6408         boolean fromexisting = false;
6409 
6410         if (widthMode == MeasureSpec.EXACTLY) {
6411             // Parent has told us how big to be. So be it.
6412             width = widthSize;
6413         } else {
6414             if (mLayout != null && mEllipsize == null) {
6415                 des = desired(mLayout);
6416             }
6417 
6418             if (des < 0) {
6419                 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
6420                 if (boring != null) {
6421                     mBoring = boring;
6422                 }
6423             } else {
6424                 fromexisting = true;
6425             }
6426 
6427             if (boring == null || boring == UNKNOWN_BORING) {
6428                 if (des < 0) {
6429                     des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
6430                 }
6431 
6432                 width = des;
6433             } else {
6434                 width = boring.width;
6435             }
6436 
6437             final Drawables dr = mDrawables;
6438             if (dr != null) {
6439                 width = Math.max(width, dr.mDrawableWidthTop);
6440                 width = Math.max(width, dr.mDrawableWidthBottom);
6441             }
6442 
6443             if (mHint != null) {
6444                 int hintDes = -1;
6445                 int hintWidth;
6446 
6447                 if (mHintLayout != null && mEllipsize == null) {
6448                     hintDes = desired(mHintLayout);
6449                 }
6450 
6451                 if (hintDes < 0) {
6452                     hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring);
6453                     if (hintBoring != null) {
6454                         mHintBoring = hintBoring;
6455                     }
6456                 }
6457 
6458                 if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
6459                     if (hintDes < 0) {
6460                         hintDes = (int) FloatMath.ceil(
6461                                 Layout.getDesiredWidth(mHint, mTextPaint));
6462                     }
6463 
6464                     hintWidth = hintDes;
6465                 } else {
6466                     hintWidth = hintBoring.width;
6467                 }
6468 
6469                 if (hintWidth > width) {
6470                     width = hintWidth;
6471                 }
6472             }
6473 
6474             width += getCompoundPaddingLeft() + getCompoundPaddingRight();
6475 
6476             if (mMaxWidthMode == EMS) {
6477                 width = Math.min(width, mMaxWidth * getLineHeight());
6478             } else {
6479                 width = Math.min(width, mMaxWidth);
6480             }
6481 
6482             if (mMinWidthMode == EMS) {
6483                 width = Math.max(width, mMinWidth * getLineHeight());
6484             } else {
6485                 width = Math.max(width, mMinWidth);
6486             }
6487 
6488             // Check against our minimum width
6489             width = Math.max(width, getSuggestedMinimumWidth());
6490 
6491             if (widthMode == MeasureSpec.AT_MOST) {
6492                 width = Math.min(widthSize, width);
6493             }
6494         }
6495 
6496         int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
6497         int unpaddedWidth = want;
6498 
6499         if (mHorizontallyScrolling) want = VERY_WIDE;
6500 
6501         int hintWant = want;
6502         int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
6503 
6504         if (mLayout == null) {
6505             makeNewLayout(want, hintWant, boring, hintBoring,
6506                           width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
6507         } else {
6508             final boolean layoutChanged = (mLayout.getWidth() != want) ||
6509                     (hintWidth != hintWant) ||
6510                     (mLayout.getEllipsizedWidth() !=
6511                             width - getCompoundPaddingLeft() - getCompoundPaddingRight());
6512 
6513             final boolean widthChanged = (mHint == null) &&
6514                     (mEllipsize == null) &&
6515                     (want > mLayout.getWidth()) &&
6516                     (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));
6517 
6518             final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
6519 
6520             if (layoutChanged || maximumChanged) {
6521                 if (!maximumChanged && widthChanged) {
6522                     mLayout.increaseWidthTo(want);
6523                 } else {
6524                     makeNewLayout(want, hintWant, boring, hintBoring,
6525                             width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
6526                 }
6527             } else {
6528                 // Nothing has changed
6529             }
6530         }
6531 
6532         if (heightMode == MeasureSpec.EXACTLY) {
6533             // Parent has told us how big to be. So be it.
6534             height = heightSize;
6535             mDesiredHeightAtMeasure = -1;
6536         } else {
6537             int desired = getDesiredHeight();
6538 
6539             height = desired;
6540             mDesiredHeightAtMeasure = desired;
6541 
6542             if (heightMode == MeasureSpec.AT_MOST) {
6543                 height = Math.min(desired, heightSize);
6544             }
6545         }
6546 
6547         int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
6548         if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
6549             unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
6550         }
6551 
6552         /*
6553          * We didn't let makeNewLayout() register to bring the cursor into view,
6554          * so do it here if there is any possibility that it is needed.
6555          */
6556         if (mMovement != null ||
6557             mLayout.getWidth() > unpaddedWidth ||
6558             mLayout.getHeight() > unpaddedHeight) {
6559             registerForPreDraw();
6560         } else {
6561             scrollTo(0, 0);
6562         }
6563 
6564         setMeasuredDimension(width, height);
6565     }
6566 
getDesiredHeight()6567     private int getDesiredHeight() {
6568         return Math.max(
6569                 getDesiredHeight(mLayout, true),
6570                 getDesiredHeight(mHintLayout, mEllipsize != null));
6571     }
6572 
getDesiredHeight(Layout layout, boolean cap)6573     private int getDesiredHeight(Layout layout, boolean cap) {
6574         if (layout == null) {
6575             return 0;
6576         }
6577 
6578         int linecount = layout.getLineCount();
6579         int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
6580         int desired = layout.getLineTop(linecount);
6581 
6582         final Drawables dr = mDrawables;
6583         if (dr != null) {
6584             desired = Math.max(desired, dr.mDrawableHeightLeft);
6585             desired = Math.max(desired, dr.mDrawableHeightRight);
6586         }
6587 
6588         desired += pad;
6589 
6590         if (mMaxMode == LINES) {
6591             /*
6592              * Don't cap the hint to a certain number of lines.
6593              * (Do cap it, though, if we have a maximum pixel height.)
6594              */
6595             if (cap) {
6596                 if (linecount > mMaximum) {
6597                     desired = layout.getLineTop(mMaximum);
6598 
6599                     if (dr != null) {
6600                         desired = Math.max(desired, dr.mDrawableHeightLeft);
6601                         desired = Math.max(desired, dr.mDrawableHeightRight);
6602                     }
6603 
6604                     desired += pad;
6605                     linecount = mMaximum;
6606                 }
6607             }
6608         } else {
6609             desired = Math.min(desired, mMaximum);
6610         }
6611 
6612         if (mMinMode == LINES) {
6613             if (linecount < mMinimum) {
6614                 desired += getLineHeight() * (mMinimum - linecount);
6615             }
6616         } else {
6617             desired = Math.max(desired, mMinimum);
6618         }
6619 
6620         // Check against our minimum height
6621         desired = Math.max(desired, getSuggestedMinimumHeight());
6622 
6623         return desired;
6624     }
6625 
6626     /**
6627      * Check whether a change to the existing text layout requires a
6628      * new view layout.
6629      */
checkForResize()6630     private void checkForResize() {
6631         boolean sizeChanged = false;
6632 
6633         if (mLayout != null) {
6634             // Check if our width changed
6635             if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
6636                 sizeChanged = true;
6637                 invalidate();
6638             }
6639 
6640             // Check if our height changed
6641             if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
6642                 int desiredHeight = getDesiredHeight();
6643 
6644                 if (desiredHeight != this.getHeight()) {
6645                     sizeChanged = true;
6646                 }
6647             } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
6648                 if (mDesiredHeightAtMeasure >= 0) {
6649                     int desiredHeight = getDesiredHeight();
6650 
6651                     if (desiredHeight != mDesiredHeightAtMeasure) {
6652                         sizeChanged = true;
6653                     }
6654                 }
6655             }
6656         }
6657 
6658         if (sizeChanged) {
6659             requestLayout();
6660             // caller will have already invalidated
6661         }
6662     }
6663 
6664     /**
6665      * Check whether entirely new text requires a new view layout
6666      * or merely a new text layout.
6667      */
checkForRelayout()6668     private void checkForRelayout() {
6669         // If we have a fixed width, we can just swap in a new text layout
6670         // if the text height stays the same or if the view height is fixed.
6671 
6672         if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6673                 (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6674                 (mHint == null || mHintLayout != null) &&
6675                 (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6676             // Static width, so try making a new text layout.
6677 
6678             int oldht = mLayout.getHeight();
6679             int want = mLayout.getWidth();
6680             int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6681 
6682             /*
6683              * No need to bring the text into view, since the size is not
6684              * changing (unless we do the requestLayout(), in which case it
6685              * will happen at measure).
6686              */
6687             makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
6688                           mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6689                           false);
6690 
6691             if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6692                 // In a fixed-height view, so use our new text layout.
6693                 if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
6694                     mLayoutParams.height != LayoutParams.MATCH_PARENT) {
6695                     invalidate();
6696                     return;
6697                 }
6698 
6699                 // Dynamic height, but height has stayed the same,
6700                 // so use our new text layout.
6701                 if (mLayout.getHeight() == oldht &&
6702                     (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6703                     invalidate();
6704                     return;
6705                 }
6706             }
6707 
6708             // We lose: the height has changed and we have a dynamic height.
6709             // Request a new view layout using our new text layout.
6710             requestLayout();
6711             invalidate();
6712         } else {
6713             // Dynamic width, so we have no choice but to request a new
6714             // view layout with a new text layout.
6715             nullLayouts();
6716             requestLayout();
6717             invalidate();
6718         }
6719     }
6720 
6721     /**
6722      * Returns true if anything changed.
6723      */
bringTextIntoView()6724     private boolean bringTextIntoView() {
6725         int line = 0;
6726         if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6727             line = mLayout.getLineCount() - 1;
6728         }
6729 
6730         Layout.Alignment a = mLayout.getParagraphAlignment(line);
6731         int dir = mLayout.getParagraphDirection(line);
6732         int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6733         int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6734         int ht = mLayout.getHeight();
6735 
6736         int scrollx, scrolly;
6737 
6738         // Convert to left, center, or right alignment.
6739         if (a == Layout.Alignment.ALIGN_NORMAL) {
6740             a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_LEFT :
6741                 Layout.Alignment.ALIGN_RIGHT;
6742         } else if (a == Layout.Alignment.ALIGN_OPPOSITE){
6743             a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_RIGHT :
6744                 Layout.Alignment.ALIGN_LEFT;
6745         }
6746 
6747         if (a == Layout.Alignment.ALIGN_CENTER) {
6748             /*
6749              * Keep centered if possible, or, if it is too wide to fit,
6750              * keep leading edge in view.
6751              */
6752 
6753             int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6754             int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6755 
6756             if (right - left < hspace) {
6757                 scrollx = (right + left) / 2 - hspace / 2;
6758             } else {
6759                 if (dir < 0) {
6760                     scrollx = right - hspace;
6761                 } else {
6762                     scrollx = left;
6763                 }
6764             }
6765         } else if (a == Layout.Alignment.ALIGN_RIGHT) {
6766             int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6767             scrollx = right - hspace;
6768         } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
6769             scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
6770         }
6771 
6772         if (ht < vspace) {
6773             scrolly = 0;
6774         } else {
6775             if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6776                 scrolly = ht - vspace;
6777             } else {
6778                 scrolly = 0;
6779             }
6780         }
6781 
6782         if (scrollx != mScrollX || scrolly != mScrollY) {
6783             scrollTo(scrollx, scrolly);
6784             return true;
6785         } else {
6786             return false;
6787         }
6788     }
6789 
6790     /**
6791      * Move the point, specified by the offset, into the view if it is needed.
6792      * This has to be called after layout. Returns true if anything changed.
6793      */
bringPointIntoView(int offset)6794     public boolean bringPointIntoView(int offset) {
6795         boolean changed = false;
6796 
6797         if (mLayout == null) return changed;
6798 
6799         int line = mLayout.getLineForOffset(offset);
6800 
6801         // FIXME: Is it okay to truncate this, or should we round?
6802         final int x = (int)mLayout.getPrimaryHorizontal(offset);
6803         final int top = mLayout.getLineTop(line);
6804         final int bottom = mLayout.getLineTop(line + 1);
6805 
6806         int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6807         int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6808         int ht = mLayout.getHeight();
6809 
6810         int grav;
6811 
6812         switch (mLayout.getParagraphAlignment(line)) {
6813             case ALIGN_LEFT:
6814                 grav = 1;
6815                 break;
6816             case ALIGN_RIGHT:
6817                 grav = -1;
6818                 break;
6819             case ALIGN_NORMAL:
6820                 grav = mLayout.getParagraphDirection(line);
6821                 break;
6822             case ALIGN_OPPOSITE:
6823                 grav = -mLayout.getParagraphDirection(line);
6824                 break;
6825             case ALIGN_CENTER:
6826             default:
6827                 grav = 0;
6828                 break;
6829         }
6830 
6831         int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6832         int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6833 
6834         int hslack = (bottom - top) / 2;
6835         int vslack = hslack;
6836 
6837         if (vslack > vspace / 4)
6838             vslack = vspace / 4;
6839         if (hslack > hspace / 4)
6840             hslack = hspace / 4;
6841 
6842         int hs = mScrollX;
6843         int vs = mScrollY;
6844 
6845         if (top - vs < vslack)
6846             vs = top - vslack;
6847         if (bottom - vs > vspace - vslack)
6848             vs = bottom - (vspace - vslack);
6849         if (ht - vs < vspace)
6850             vs = ht - vspace;
6851         if (0 - vs > 0)
6852             vs = 0;
6853 
6854         if (grav != 0) {
6855             if (x - hs < hslack) {
6856                 hs = x - hslack;
6857             }
6858             if (x - hs > hspace - hslack) {
6859                 hs = x - (hspace - hslack);
6860             }
6861         }
6862 
6863         if (grav < 0) {
6864             if (left - hs > 0)
6865                 hs = left;
6866             if (right - hs < hspace)
6867                 hs = right - hspace;
6868         } else if (grav > 0) {
6869             if (right - hs < hspace)
6870                 hs = right - hspace;
6871             if (left - hs > 0)
6872                 hs = left;
6873         } else /* grav == 0 */ {
6874             if (right - left <= hspace) {
6875                 /*
6876                  * If the entire text fits, center it exactly.
6877                  */
6878                 hs = left - (hspace - (right - left)) / 2;
6879             } else if (x > right - hslack) {
6880                 /*
6881                  * If we are near the right edge, keep the right edge
6882                  * at the edge of the view.
6883                  */
6884                 hs = right - hspace;
6885             } else if (x < left + hslack) {
6886                 /*
6887                  * If we are near the left edge, keep the left edge
6888                  * at the edge of the view.
6889                  */
6890                 hs = left;
6891             } else if (left > hs) {
6892                 /*
6893                  * Is there whitespace visible at the left?  Fix it if so.
6894                  */
6895                 hs = left;
6896             } else if (right < hs + hspace) {
6897                 /*
6898                  * Is there whitespace visible at the right?  Fix it if so.
6899                  */
6900                 hs = right - hspace;
6901             } else {
6902                 /*
6903                  * Otherwise, float as needed.
6904                  */
6905                 if (x - hs < hslack) {
6906                     hs = x - hslack;
6907                 }
6908                 if (x - hs > hspace - hslack) {
6909                     hs = x - (hspace - hslack);
6910                 }
6911             }
6912         }
6913 
6914         if (hs != mScrollX || vs != mScrollY) {
6915             if (mScroller == null) {
6916                 scrollTo(hs, vs);
6917             } else {
6918                 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
6919                 int dx = hs - mScrollX;
6920                 int dy = vs - mScrollY;
6921 
6922                 if (duration > ANIMATED_SCROLL_GAP) {
6923                     mScroller.startScroll(mScrollX, mScrollY, dx, dy);
6924                     awakenScrollBars(mScroller.getDuration());
6925                     invalidate();
6926                 } else {
6927                     if (!mScroller.isFinished()) {
6928                         mScroller.abortAnimation();
6929                     }
6930 
6931                     scrollBy(dx, dy);
6932                 }
6933 
6934                 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
6935             }
6936 
6937             changed = true;
6938         }
6939 
6940         if (isFocused()) {
6941             // This offsets because getInterestingRect() is in terms of viewport coordinates, but
6942             // requestRectangleOnScreen() is in terms of content coordinates.
6943 
6944             if (mTempRect == null) mTempRect = new Rect();
6945             // The offsets here are to ensure the rectangle we are using is
6946             // within our view bounds, in case the cursor is on the far left
6947             // or right.  If it isn't withing the bounds, then this request
6948             // will be ignored.
6949             mTempRect.set(x - 2, top, x + 2, bottom);
6950             getInterestingRect(mTempRect, line);
6951             mTempRect.offset(mScrollX, mScrollY);
6952 
6953             if (requestRectangleOnScreen(mTempRect)) {
6954                 changed = true;
6955             }
6956         }
6957 
6958         return changed;
6959     }
6960 
6961     /**
6962      * Move the cursor, if needed, so that it is at an offset that is visible
6963      * to the user.  This will not move the cursor if it represents more than
6964      * one character (a selection range).  This will only work if the
6965      * TextView contains spannable text; otherwise it will do nothing.
6966      *
6967      * @return True if the cursor was actually moved, false otherwise.
6968      */
moveCursorToVisibleOffset()6969     public boolean moveCursorToVisibleOffset() {
6970         if (!(mText instanceof Spannable)) {
6971             return false;
6972         }
6973         int start = getSelectionStart();
6974         int end = getSelectionEnd();
6975         if (start != end) {
6976             return false;
6977         }
6978 
6979         // First: make sure the line is visible on screen:
6980 
6981         int line = mLayout.getLineForOffset(start);
6982 
6983         final int top = mLayout.getLineTop(line);
6984         final int bottom = mLayout.getLineTop(line + 1);
6985         final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6986         int vslack = (bottom - top) / 2;
6987         if (vslack > vspace / 4)
6988             vslack = vspace / 4;
6989         final int vs = mScrollY;
6990 
6991         if (top < (vs+vslack)) {
6992             line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
6993         } else if (bottom > (vspace+vs-vslack)) {
6994             line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
6995         }
6996 
6997         // Next: make sure the character is visible on screen:
6998 
6999         final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
7000         final int hs = mScrollX;
7001         final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
7002         final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
7003 
7004         // line might contain bidirectional text
7005         final int lowChar = leftChar < rightChar ? leftChar : rightChar;
7006         final int highChar = leftChar > rightChar ? leftChar : rightChar;
7007 
7008         int newStart = start;
7009         if (newStart < lowChar) {
7010             newStart = lowChar;
7011         } else if (newStart > highChar) {
7012             newStart = highChar;
7013         }
7014 
7015         if (newStart != start) {
7016             Selection.setSelection((Spannable)mText, newStart);
7017             return true;
7018         }
7019 
7020         return false;
7021     }
7022 
7023     @Override
computeScroll()7024     public void computeScroll() {
7025         if (mScroller != null) {
7026             if (mScroller.computeScrollOffset()) {
7027                 mScrollX = mScroller.getCurrX();
7028                 mScrollY = mScroller.getCurrY();
7029                 invalidateParentCaches();
7030                 postInvalidate();  // So we draw again
7031             }
7032         }
7033     }
7034 
getInterestingRect(Rect r, int line)7035     private void getInterestingRect(Rect r, int line) {
7036         convertFromViewportToContentCoordinates(r);
7037 
7038         // Rectangle can can be expanded on first and last line to take
7039         // padding into account.
7040         // TODO Take left/right padding into account too?
7041         if (line == 0) r.top -= getExtendedPaddingTop();
7042         if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
7043     }
7044 
convertFromViewportToContentCoordinates(Rect r)7045     private void convertFromViewportToContentCoordinates(Rect r) {
7046         final int horizontalOffset = viewportToContentHorizontalOffset();
7047         r.left += horizontalOffset;
7048         r.right += horizontalOffset;
7049 
7050         final int verticalOffset = viewportToContentVerticalOffset();
7051         r.top += verticalOffset;
7052         r.bottom += verticalOffset;
7053     }
7054 
viewportToContentHorizontalOffset()7055     private int viewportToContentHorizontalOffset() {
7056         return getCompoundPaddingLeft() - mScrollX;
7057     }
7058 
viewportToContentVerticalOffset()7059     private int viewportToContentVerticalOffset() {
7060         int offset = getExtendedPaddingTop() - mScrollY;
7061         if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
7062             offset += getVerticalOffset(false);
7063         }
7064         return offset;
7065     }
7066 
7067     @Override
debug(int depth)7068     public void debug(int depth) {
7069         super.debug(depth);
7070 
7071         String output = debugIndent(depth);
7072         output += "frame={" + mLeft + ", " + mTop + ", " + mRight
7073                 + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
7074                 + "} ";
7075 
7076         if (mText != null) {
7077 
7078             output += "mText=\"" + mText + "\" ";
7079             if (mLayout != null) {
7080                 output += "mLayout width=" + mLayout.getWidth()
7081                         + " height=" + mLayout.getHeight();
7082             }
7083         } else {
7084             output += "mText=NULL";
7085         }
7086         Log.d(VIEW_LOG_TAG, output);
7087     }
7088 
7089     /**
7090      * Convenience for {@link Selection#getSelectionStart}.
7091      */
7092     @ViewDebug.ExportedProperty(category = "text")
getSelectionStart()7093     public int getSelectionStart() {
7094         return Selection.getSelectionStart(getText());
7095     }
7096 
7097     /**
7098      * Convenience for {@link Selection#getSelectionEnd}.
7099      */
7100     @ViewDebug.ExportedProperty(category = "text")
getSelectionEnd()7101     public int getSelectionEnd() {
7102         return Selection.getSelectionEnd(getText());
7103     }
7104 
7105     /**
7106      * Return true iff there is a selection inside this text view.
7107      */
hasSelection()7108     public boolean hasSelection() {
7109         final int selectionStart = getSelectionStart();
7110         final int selectionEnd = getSelectionEnd();
7111 
7112         return selectionStart >= 0 && selectionStart != selectionEnd;
7113     }
7114 
7115     /**
7116      * Sets the properties of this field (lines, horizontally scrolling,
7117      * transformation method) to be for a single-line input.
7118      *
7119      * @attr ref android.R.styleable#TextView_singleLine
7120      */
setSingleLine()7121     public void setSingleLine() {
7122         setSingleLine(true);
7123     }
7124 
7125     /**
7126      * Sets the properties of this field to transform input to ALL CAPS
7127      * display. This may use a "small caps" formatting if available.
7128      * This setting will be ignored if this field is editable or selectable.
7129      *
7130      * This call replaces the current transformation method. Disabling this
7131      * will not necessarily restore the previous behavior from before this
7132      * was enabled.
7133      *
7134      * @see #setTransformationMethod(TransformationMethod)
7135      * @attr ref android.R.styleable#TextView_textAllCaps
7136      */
setAllCaps(boolean allCaps)7137     public void setAllCaps(boolean allCaps) {
7138         if (allCaps) {
7139             setTransformationMethod(new AllCapsTransformationMethod(getContext()));
7140         } else {
7141             setTransformationMethod(null);
7142         }
7143     }
7144 
7145     /**
7146      * If true, sets the properties of this field (number of lines, horizontally scrolling,
7147      * transformation method) to be for a single-line input; if false, restores these to the default
7148      * conditions.
7149      *
7150      * Note that the default conditions are not necessarily those that were in effect prior this
7151      * method, and you may want to reset these properties to your custom values.
7152      *
7153      * @attr ref android.R.styleable#TextView_singleLine
7154      */
7155     @android.view.RemotableViewMethod
setSingleLine(boolean singleLine)7156     public void setSingleLine(boolean singleLine) {
7157         // Could be used, but may break backward compatibility.
7158         // if (mSingleLine == singleLine) return;
7159         setInputTypeSingleLine(singleLine);
7160         applySingleLine(singleLine, true, true);
7161     }
7162 
7163     /**
7164      * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
7165      * @param singleLine
7166      */
setInputTypeSingleLine(boolean singleLine)7167     private void setInputTypeSingleLine(boolean singleLine) {
7168         if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
7169             if (singleLine) {
7170                 mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
7171             } else {
7172                 mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
7173             }
7174         }
7175     }
7176 
applySingleLine(boolean singleLine, boolean applyTransformation, boolean changeMaxLines)7177     private void applySingleLine(boolean singleLine, boolean applyTransformation,
7178             boolean changeMaxLines) {
7179         mSingleLine = singleLine;
7180         if (singleLine) {
7181             setLines(1);
7182             setHorizontallyScrolling(true);
7183             if (applyTransformation) {
7184                 setTransformationMethod(SingleLineTransformationMethod.getInstance());
7185             }
7186         } else {
7187             if (changeMaxLines) {
7188                 setMaxLines(Integer.MAX_VALUE);
7189             }
7190             setHorizontallyScrolling(false);
7191             if (applyTransformation) {
7192                 setTransformationMethod(null);
7193             }
7194         }
7195     }
7196 
7197     /**
7198      * Causes words in the text that are longer than the view is wide
7199      * to be ellipsized instead of broken in the middle.  You may also
7200      * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
7201      * to constrain the text to a single line.  Use <code>null</code>
7202      * to turn off ellipsizing.
7203      *
7204      * If {@link #setMaxLines} has been used to set two or more lines,
7205      * {@link android.text.TextUtils.TruncateAt#END} and
7206      * {@link android.text.TextUtils.TruncateAt#MARQUEE}* are only supported
7207      * (other ellipsizing types will not do anything).
7208      *
7209      * @attr ref android.R.styleable#TextView_ellipsize
7210      */
setEllipsize(TextUtils.TruncateAt where)7211     public void setEllipsize(TextUtils.TruncateAt where) {
7212         // TruncateAt is an enum. != comparison is ok between these singleton objects.
7213         if (mEllipsize != where) {
7214             mEllipsize = where;
7215 
7216             if (mLayout != null) {
7217                 nullLayouts();
7218                 requestLayout();
7219                 invalidate();
7220             }
7221         }
7222     }
7223 
7224     /**
7225      * Sets how many times to repeat the marquee animation. Only applied if the
7226      * TextView has marquee enabled. Set to -1 to repeat indefinitely.
7227      *
7228      * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
7229      */
setMarqueeRepeatLimit(int marqueeLimit)7230     public void setMarqueeRepeatLimit(int marqueeLimit) {
7231         mMarqueeRepeatLimit = marqueeLimit;
7232     }
7233 
7234     /**
7235      * Returns where, if anywhere, words that are longer than the view
7236      * is wide should be ellipsized.
7237      */
7238     @ViewDebug.ExportedProperty
getEllipsize()7239     public TextUtils.TruncateAt getEllipsize() {
7240         return mEllipsize;
7241     }
7242 
7243     /**
7244      * Set the TextView so that when it takes focus, all the text is
7245      * selected.
7246      *
7247      * @attr ref android.R.styleable#TextView_selectAllOnFocus
7248      */
7249     @android.view.RemotableViewMethod
setSelectAllOnFocus(boolean selectAllOnFocus)7250     public void setSelectAllOnFocus(boolean selectAllOnFocus) {
7251         mSelectAllOnFocus = selectAllOnFocus;
7252 
7253         if (selectAllOnFocus && !(mText instanceof Spannable)) {
7254             setText(mText, BufferType.SPANNABLE);
7255         }
7256     }
7257 
7258     /**
7259      * Set whether the cursor is visible.  The default is true.
7260      *
7261      * @attr ref android.R.styleable#TextView_cursorVisible
7262      */
7263     @android.view.RemotableViewMethod
setCursorVisible(boolean visible)7264     public void setCursorVisible(boolean visible) {
7265         if (mCursorVisible != visible) {
7266             mCursorVisible = visible;
7267             invalidate();
7268 
7269             makeBlink();
7270 
7271             // InsertionPointCursorController depends on mCursorVisible
7272             prepareCursorControllers();
7273         }
7274     }
7275 
isCursorVisible()7276     private boolean isCursorVisible() {
7277         return mCursorVisible && isTextEditable();
7278     }
7279 
canMarquee()7280     private boolean canMarquee() {
7281         int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
7282         return width > 0 && (mLayout.getLineWidth(0) > width ||
7283                 (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null &&
7284                         mSavedMarqueeModeLayout.getLineWidth(0) > width));
7285     }
7286 
startMarquee()7287     private void startMarquee() {
7288         // Do not ellipsize EditText
7289         if (mInput != null) return;
7290 
7291         if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
7292             return;
7293         }
7294 
7295         if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
7296                 getLineCount() == 1 && canMarquee()) {
7297 
7298             if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
7299                 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
7300                 final Layout tmp = mLayout;
7301                 mLayout = mSavedMarqueeModeLayout;
7302                 mSavedMarqueeModeLayout = tmp;
7303                 setHorizontalFadingEdgeEnabled(true);
7304                 requestLayout();
7305                 invalidate();
7306             }
7307 
7308             if (mMarquee == null) mMarquee = new Marquee(this);
7309             mMarquee.start(mMarqueeRepeatLimit);
7310         }
7311     }
7312 
stopMarquee()7313     private void stopMarquee() {
7314         if (mMarquee != null && !mMarquee.isStopped()) {
7315             mMarquee.stop();
7316         }
7317 
7318         if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) {
7319             mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
7320             final Layout tmp = mSavedMarqueeModeLayout;
7321             mSavedMarqueeModeLayout = mLayout;
7322             mLayout = tmp;
7323             setHorizontalFadingEdgeEnabled(false);
7324             requestLayout();
7325             invalidate();
7326         }
7327     }
7328 
startStopMarquee(boolean start)7329     private void startStopMarquee(boolean start) {
7330         if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7331             if (start) {
7332                 startMarquee();
7333             } else {
7334                 stopMarquee();
7335             }
7336         }
7337     }
7338 
7339     private static final class Marquee extends Handler {
7340         // TODO: Add an option to configure this
7341         private static final float MARQUEE_DELTA_MAX = 0.07f;
7342         private static final int MARQUEE_DELAY = 1200;
7343         private static final int MARQUEE_RESTART_DELAY = 1200;
7344         private static final int MARQUEE_RESOLUTION = 1000 / 30;
7345         private static final int MARQUEE_PIXELS_PER_SECOND = 30;
7346 
7347         private static final byte MARQUEE_STOPPED = 0x0;
7348         private static final byte MARQUEE_STARTING = 0x1;
7349         private static final byte MARQUEE_RUNNING = 0x2;
7350 
7351         private static final int MESSAGE_START = 0x1;
7352         private static final int MESSAGE_TICK = 0x2;
7353         private static final int MESSAGE_RESTART = 0x3;
7354 
7355         private final WeakReference<TextView> mView;
7356 
7357         private byte mStatus = MARQUEE_STOPPED;
7358         private final float mScrollUnit;
7359         private float mMaxScroll;
7360         float mMaxFadeScroll;
7361         private float mGhostStart;
7362         private float mGhostOffset;
7363         private float mFadeStop;
7364         private int mRepeatLimit;
7365 
7366         float mScroll;
7367 
Marquee(TextView v)7368         Marquee(TextView v) {
7369             final float density = v.getContext().getResources().getDisplayMetrics().density;
7370             mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
7371             mView = new WeakReference<TextView>(v);
7372         }
7373 
7374         @Override
handleMessage(Message msg)7375         public void handleMessage(Message msg) {
7376             switch (msg.what) {
7377                 case MESSAGE_START:
7378                     mStatus = MARQUEE_RUNNING;
7379                     tick();
7380                     break;
7381                 case MESSAGE_TICK:
7382                     tick();
7383                     break;
7384                 case MESSAGE_RESTART:
7385                     if (mStatus == MARQUEE_RUNNING) {
7386                         if (mRepeatLimit >= 0) {
7387                             mRepeatLimit--;
7388                         }
7389                         start(mRepeatLimit);
7390                     }
7391                     break;
7392             }
7393         }
7394 
tick()7395         void tick() {
7396             if (mStatus != MARQUEE_RUNNING) {
7397                 return;
7398             }
7399 
7400             removeMessages(MESSAGE_TICK);
7401 
7402             final TextView textView = mView.get();
7403             if (textView != null && (textView.isFocused() || textView.isSelected())) {
7404                 mScroll += mScrollUnit;
7405                 if (mScroll > mMaxScroll) {
7406                     mScroll = mMaxScroll;
7407                     sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
7408                 } else {
7409                     sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
7410                 }
7411                 textView.invalidate();
7412             }
7413         }
7414 
stop()7415         void stop() {
7416             mStatus = MARQUEE_STOPPED;
7417             removeMessages(MESSAGE_START);
7418             removeMessages(MESSAGE_RESTART);
7419             removeMessages(MESSAGE_TICK);
7420             resetScroll();
7421         }
7422 
resetScroll()7423         private void resetScroll() {
7424             mScroll = 0.0f;
7425             final TextView textView = mView.get();
7426             if (textView != null) textView.invalidate();
7427         }
7428 
start(int repeatLimit)7429         void start(int repeatLimit) {
7430             if (repeatLimit == 0) {
7431                 stop();
7432                 return;
7433             }
7434             mRepeatLimit = repeatLimit;
7435             final TextView textView = mView.get();
7436             if (textView != null && textView.mLayout != null) {
7437                 mStatus = MARQUEE_STARTING;
7438                 mScroll = 0.0f;
7439                 final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
7440                         textView.getCompoundPaddingRight();
7441                 final float lineWidth = textView.mLayout.getLineWidth(0);
7442                 final float gap = textWidth / 3.0f;
7443                 mGhostStart = lineWidth - textWidth + gap;
7444                 mMaxScroll = mGhostStart + textWidth;
7445                 mGhostOffset = lineWidth + gap;
7446                 mFadeStop = lineWidth + textWidth / 6.0f;
7447                 mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
7448 
7449                 textView.invalidate();
7450                 sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
7451             }
7452         }
7453 
getGhostOffset()7454         float getGhostOffset() {
7455             return mGhostOffset;
7456         }
7457 
shouldDrawLeftFade()7458         boolean shouldDrawLeftFade() {
7459             return mScroll <= mFadeStop;
7460         }
7461 
shouldDrawGhost()7462         boolean shouldDrawGhost() {
7463             return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
7464         }
7465 
isRunning()7466         boolean isRunning() {
7467             return mStatus == MARQUEE_RUNNING;
7468         }
7469 
isStopped()7470         boolean isStopped() {
7471             return mStatus == MARQUEE_STOPPED;
7472         }
7473     }
7474 
7475     /**
7476      * This method is called when the text is changed, in case any subclasses
7477      * would like to know.
7478      *
7479      * Within <code>text</code>, the <code>lengthAfter</code> characters
7480      * beginning at <code>start</code> have just replaced old text that had
7481      * length <code>lengthBefore</code>. It is an error to attempt to make
7482      * changes to <code>text</code> from this callback.
7483      *
7484      * @param text The text the TextView is displaying
7485      * @param start The offset of the start of the range of the text that was
7486      * modified
7487      * @param lengthBefore The length of the former text that has been replaced
7488      * @param lengthAfter The length of the replacement modified text
7489      */
onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)7490     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
7491         // intentionally empty, template pattern method can be overridden by subclasses
7492     }
7493 
7494     /**
7495      * This method is called when the selection has changed, in case any
7496      * subclasses would like to know.
7497      *
7498      * @param selStart The new selection start location.
7499      * @param selEnd The new selection end location.
7500      */
onSelectionChanged(int selStart, int selEnd)7501     protected void onSelectionChanged(int selStart, int selEnd) {
7502         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
7503     }
7504 
7505     /**
7506      * Adds a TextWatcher to the list of those whose methods are called
7507      * whenever this TextView's text changes.
7508      * <p>
7509      * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
7510      * not called after {@link #setText} calls.  Now, doing {@link #setText}
7511      * if there are any text changed listeners forces the buffer type to
7512      * Editable if it would not otherwise be and does call this method.
7513      */
addTextChangedListener(TextWatcher watcher)7514     public void addTextChangedListener(TextWatcher watcher) {
7515         if (mListeners == null) {
7516             mListeners = new ArrayList<TextWatcher>();
7517         }
7518 
7519         mListeners.add(watcher);
7520     }
7521 
7522     /**
7523      * Removes the specified TextWatcher from the list of those whose
7524      * methods are called
7525      * whenever this TextView's text changes.
7526      */
removeTextChangedListener(TextWatcher watcher)7527     public void removeTextChangedListener(TextWatcher watcher) {
7528         if (mListeners != null) {
7529             int i = mListeners.indexOf(watcher);
7530 
7531             if (i >= 0) {
7532                 mListeners.remove(i);
7533             }
7534         }
7535     }
7536 
sendBeforeTextChanged(CharSequence text, int start, int before, int after)7537     private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
7538         if (mListeners != null) {
7539             final ArrayList<TextWatcher> list = mListeners;
7540             final int count = list.size();
7541             for (int i = 0; i < count; i++) {
7542                 list.get(i).beforeTextChanged(text, start, before, after);
7543             }
7544         }
7545 
7546         // The spans that are inside or intersect the modified region no longer make sense
7547         removeIntersectingSpans(start, start + before, SpellCheckSpan.class);
7548         removeIntersectingSpans(start, start + before, SuggestionSpan.class);
7549     }
7550 
7551     // Removes all spans that are inside or actually overlap the start..end range
removeIntersectingSpans(int start, int end, Class<T> type)7552     private <T> void removeIntersectingSpans(int start, int end, Class<T> type) {
7553         if (!(mText instanceof Editable)) return;
7554         Editable text = (Editable) mText;
7555 
7556         T[] spans = text.getSpans(start, end, type);
7557         final int length = spans.length;
7558         for (int i = 0; i < length; i++) {
7559             final int s = text.getSpanStart(spans[i]);
7560             final int e = text.getSpanEnd(spans[i]);
7561             // Spans that are adjacent to the edited region will be handled in
7562             // updateSpellCheckSpans. Result depends on what will be added (space or text)
7563             if (e == start || s == end) break;
7564             text.removeSpan(spans[i]);
7565         }
7566     }
7567 
7568     /**
7569      * Not private so it can be called from an inner class without going
7570      * through a thunk.
7571      */
sendOnTextChanged(CharSequence text, int start, int before, int after)7572     void sendOnTextChanged(CharSequence text, int start, int before, int after) {
7573         if (mListeners != null) {
7574             final ArrayList<TextWatcher> list = mListeners;
7575             final int count = list.size();
7576             for (int i = 0; i < count; i++) {
7577                 list.get(i).onTextChanged(text, start, before, after);
7578             }
7579         }
7580     }
7581 
7582     /**
7583      * Not private so it can be called from an inner class without going
7584      * through a thunk.
7585      */
sendAfterTextChanged(Editable text)7586     void sendAfterTextChanged(Editable text) {
7587         if (mListeners != null) {
7588             final ArrayList<TextWatcher> list = mListeners;
7589             final int count = list.size();
7590             for (int i = 0; i < count; i++) {
7591                 list.get(i).afterTextChanged(text);
7592             }
7593         }
7594     }
7595 
7596     /**
7597      * Not private so it can be called from an inner class without going
7598      * through a thunk.
7599      */
handleTextChanged(CharSequence buffer, int start, int before, int after)7600     void handleTextChanged(CharSequence buffer, int start, int before, int after) {
7601         final InputMethodState ims = mInputMethodState;
7602         if (ims == null || ims.mBatchEditNesting == 0) {
7603             updateAfterEdit();
7604         }
7605         if (ims != null) {
7606             ims.mContentChanged = true;
7607             if (ims.mChangedStart < 0) {
7608                 ims.mChangedStart = start;
7609                 ims.mChangedEnd = start+before;
7610             } else {
7611                 ims.mChangedStart = Math.min(ims.mChangedStart, start);
7612                 ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
7613             }
7614             ims.mChangedDelta += after-before;
7615         }
7616 
7617         sendOnTextChanged(buffer, start, before, after);
7618         onTextChanged(buffer, start, before, after);
7619 
7620         updateSpellCheckSpans(start, start + after);
7621 
7622         // Hide the controllers if the amount of content changed
7623         if (before != after) {
7624             // We do not hide the span controllers, as they can be added when a new text is
7625             // inserted into the text view
7626             hideCursorControllers();
7627         }
7628     }
7629 
7630     /**
7631      * Not private so it can be called from an inner class without going
7632      * through a thunk.
7633      */
spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd)7634     void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
7635         // XXX Make the start and end move together if this ends up
7636         // spending too much time invalidating.
7637 
7638         boolean selChanged = false;
7639         int newSelStart=-1, newSelEnd=-1;
7640 
7641         final InputMethodState ims = mInputMethodState;
7642 
7643         if (what == Selection.SELECTION_END) {
7644             mHighlightPathBogus = true;
7645             selChanged = true;
7646             newSelEnd = newStart;
7647 
7648             if (!isFocused()) {
7649                 mSelectionMoved = true;
7650             }
7651 
7652             if (oldStart >= 0 || newStart >= 0) {
7653                 invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
7654                 registerForPreDraw();
7655                 makeBlink();
7656             }
7657         }
7658 
7659         if (what == Selection.SELECTION_START) {
7660             mHighlightPathBogus = true;
7661             selChanged = true;
7662             newSelStart = newStart;
7663 
7664             if (!isFocused()) {
7665                 mSelectionMoved = true;
7666             }
7667 
7668             if (oldStart >= 0 || newStart >= 0) {
7669                 int end = Selection.getSelectionEnd(buf);
7670                 invalidateCursor(end, oldStart, newStart);
7671             }
7672         }
7673 
7674         if (selChanged) {
7675             if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
7676                 if (newSelStart < 0) {
7677                     newSelStart = Selection.getSelectionStart(buf);
7678                 }
7679                 if (newSelEnd < 0) {
7680                     newSelEnd = Selection.getSelectionEnd(buf);
7681                 }
7682                 onSelectionChanged(newSelStart, newSelEnd);
7683             }
7684         }
7685 
7686         if (what instanceof UpdateAppearance || what instanceof ParagraphStyle
7687                 || (what instanceof SuggestionSpan && (((SuggestionSpan)what).getFlags()
7688                         & SuggestionSpan.FLAG_AUTO_CORRECTION) != 0)) {
7689             if (ims == null || ims.mBatchEditNesting == 0) {
7690                 invalidate();
7691                 mHighlightPathBogus = true;
7692                 checkForResize();
7693             } else {
7694                 ims.mContentChanged = true;
7695             }
7696         }
7697 
7698         if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
7699             mHighlightPathBogus = true;
7700             if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
7701                 ims.mSelectionModeChanged = true;
7702             }
7703 
7704             if (Selection.getSelectionStart(buf) >= 0) {
7705                 if (ims == null || ims.mBatchEditNesting == 0) {
7706                     invalidateCursor();
7707                 } else {
7708                     ims.mCursorChanged = true;
7709                 }
7710             }
7711         }
7712 
7713         if (what instanceof ParcelableSpan) {
7714             // If this is a span that can be sent to a remote process,
7715             // the current extract editor would be interested in it.
7716             if (ims != null && ims.mExtracting != null) {
7717                 if (ims.mBatchEditNesting != 0) {
7718                     if (oldStart >= 0) {
7719                         if (ims.mChangedStart > oldStart) {
7720                             ims.mChangedStart = oldStart;
7721                         }
7722                         if (ims.mChangedStart > oldEnd) {
7723                             ims.mChangedStart = oldEnd;
7724                         }
7725                     }
7726                     if (newStart >= 0) {
7727                         if (ims.mChangedStart > newStart) {
7728                             ims.mChangedStart = newStart;
7729                         }
7730                         if (ims.mChangedStart > newEnd) {
7731                             ims.mChangedStart = newEnd;
7732                         }
7733                     }
7734                 } else {
7735                     if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
7736                             + oldStart + "-" + oldEnd + ","
7737                             + newStart + "-" + newEnd + what);
7738                     ims.mContentChanged = true;
7739                 }
7740             }
7741         }
7742 
7743         if (newStart < 0 && what instanceof SpellCheckSpan) {
7744             getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what);
7745         }
7746     }
7747 
7748     /**
7749      * Create new SpellCheckSpans on the modified region.
7750      */
updateSpellCheckSpans(int start, int end)7751     private void updateSpellCheckSpans(int start, int end) {
7752         if (isTextEditable() && isSuggestionsEnabled()) {
7753             getSpellChecker().spellCheck(start, end);
7754         }
7755     }
7756 
7757     /**
7758      * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
7759      * pop-up should be displayed.
7760      */
7761     private class EasyEditSpanController {
7762 
7763         private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
7764 
7765         private EasyEditPopupWindow mPopupWindow;
7766 
7767         private EasyEditSpan mEasyEditSpan;
7768 
7769         private Runnable mHidePopup;
7770 
hide()7771         private void hide() {
7772             if (mPopupWindow != null) {
7773                 mPopupWindow.hide();
7774                 TextView.this.removeCallbacks(mHidePopup);
7775             }
7776             removeSpans(mText);
7777             mEasyEditSpan = null;
7778         }
7779 
7780         /**
7781          * Monitors the changes in the text.
7782          *
7783          * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
7784          * as the notifications are not sent when a spannable (with spans) is inserted.
7785          */
onTextChange(CharSequence buffer)7786         public void onTextChange(CharSequence buffer) {
7787             adjustSpans(mText);
7788 
7789             if (getWindowVisibility() != View.VISIBLE) {
7790                 // The window is not visible yet, ignore the text change.
7791                 return;
7792             }
7793 
7794             if (mLayout == null) {
7795                 // The view has not been layout yet, ignore the text change
7796                 return;
7797             }
7798 
7799             InputMethodManager imm = InputMethodManager.peekInstance();
7800             if (!(TextView.this instanceof ExtractEditText)
7801                     && imm != null && imm.isFullscreenMode()) {
7802                 // The input is in extract mode. We do not have to handle the easy edit in the
7803                 // original TextView, as the ExtractEditText will do
7804                 return;
7805             }
7806 
7807             // Remove the current easy edit span, as the text changed, and remove the pop-up
7808             // (if any)
7809             if (mEasyEditSpan != null) {
7810                 if (mText instanceof Spannable) {
7811                     ((Spannable) mText).removeSpan(mEasyEditSpan);
7812                 }
7813                 mEasyEditSpan = null;
7814             }
7815             if (mPopupWindow != null && mPopupWindow.isShowing()) {
7816                 mPopupWindow.hide();
7817             }
7818 
7819             // Display the new easy edit span (if any).
7820             if (buffer instanceof Spanned) {
7821                 mEasyEditSpan = getSpan((Spanned) buffer);
7822                 if (mEasyEditSpan != null) {
7823                     if (mPopupWindow == null) {
7824                         mPopupWindow = new EasyEditPopupWindow();
7825                         mHidePopup = new Runnable() {
7826                             @Override
7827                             public void run() {
7828                                 hide();
7829                             }
7830                         };
7831                     }
7832                     mPopupWindow.show(mEasyEditSpan);
7833                     TextView.this.removeCallbacks(mHidePopup);
7834                     TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
7835                 }
7836             }
7837         }
7838 
7839         /**
7840          * Adjusts the spans by removing all of them except the last one.
7841          */
adjustSpans(CharSequence buffer)7842         private void adjustSpans(CharSequence buffer) {
7843             // This method enforces that only one easy edit span is attached to the text.
7844             // A better way to enforce this would be to listen for onSpanAdded, but this method
7845             // cannot be used in this scenario as no notification is triggered when a text with
7846             // spans is inserted into a text.
7847             if (buffer instanceof Spannable) {
7848                 Spannable spannable = (Spannable) buffer;
7849                 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7850                         EasyEditSpan.class);
7851                 for (int i = 0; i < spans.length - 1; i++) {
7852                     spannable.removeSpan(spans[i]);
7853                 }
7854             }
7855         }
7856 
7857         /**
7858          * Removes all the {@link EasyEditSpan} currently attached.
7859          */
removeSpans(CharSequence buffer)7860         private void removeSpans(CharSequence buffer) {
7861             if (buffer instanceof Spannable) {
7862                 Spannable spannable = (Spannable) buffer;
7863                 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7864                         EasyEditSpan.class);
7865                 for (int i = 0; i < spans.length; i++) {
7866                     spannable.removeSpan(spans[i]);
7867                 }
7868             }
7869         }
7870 
getSpan(Spanned spanned)7871         private EasyEditSpan getSpan(Spanned spanned) {
7872             EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
7873                     EasyEditSpan.class);
7874             if (easyEditSpans.length == 0) {
7875                 return null;
7876             } else {
7877                 return easyEditSpans[0];
7878             }
7879         }
7880     }
7881 
7882     /**
7883      * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
7884      * by {@link EasyEditSpanController}.
7885      */
7886     private class EasyEditPopupWindow extends PinnedPopupWindow
7887             implements OnClickListener {
7888         private static final int POPUP_TEXT_LAYOUT =
7889                 com.android.internal.R.layout.text_edit_action_popup_text;
7890         private TextView mDeleteTextView;
7891         private EasyEditSpan mEasyEditSpan;
7892 
7893         @Override
createPopupWindow()7894         protected void createPopupWindow() {
7895             mPopupWindow = new PopupWindow(TextView.this.mContext, null,
7896                     com.android.internal.R.attr.textSelectHandleWindowStyle);
7897             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
7898             mPopupWindow.setClippingEnabled(true);
7899         }
7900 
7901         @Override
initContentView()7902         protected void initContentView() {
7903             LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
7904             linearLayout.setOrientation(LinearLayout.HORIZONTAL);
7905             mContentView = linearLayout;
7906             mContentView.setBackgroundResource(
7907                     com.android.internal.R.drawable.text_edit_side_paste_window);
7908 
7909             LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
7910                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
7911 
7912             LayoutParams wrapContent = new LayoutParams(
7913                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
7914 
7915             mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
7916             mDeleteTextView.setLayoutParams(wrapContent);
7917             mDeleteTextView.setText(com.android.internal.R.string.delete);
7918             mDeleteTextView.setOnClickListener(this);
7919             mContentView.addView(mDeleteTextView);
7920         }
7921 
show(EasyEditSpan easyEditSpan)7922         public void show(EasyEditSpan easyEditSpan) {
7923             mEasyEditSpan = easyEditSpan;
7924             super.show();
7925         }
7926 
7927         @Override
onClick(View view)7928         public void onClick(View view) {
7929             if (view == mDeleteTextView) {
7930                 deleteText();
7931             }
7932         }
7933 
deleteText()7934         private void deleteText() {
7935             Editable editable = (Editable) mText;
7936             int start = editable.getSpanStart(mEasyEditSpan);
7937             int end = editable.getSpanEnd(mEasyEditSpan);
7938             if (start >= 0 && end >= 0) {
7939                 editable.delete(start, end);
7940             }
7941         }
7942 
7943         @Override
getTextOffset()7944         protected int getTextOffset() {
7945             // Place the pop-up at the end of the span
7946             Editable editable = (Editable) mText;
7947             return editable.getSpanEnd(mEasyEditSpan);
7948         }
7949 
7950         @Override
getVerticalLocalPosition(int line)7951         protected int getVerticalLocalPosition(int line) {
7952             return mLayout.getLineBottom(line);
7953         }
7954 
7955         @Override
clipVertically(int positionY)7956         protected int clipVertically(int positionY) {
7957             // As we display the pop-up below the span, no vertical clipping is required.
7958             return positionY;
7959         }
7960     }
7961 
7962     private class ChangeWatcher implements TextWatcher, SpanWatcher {
7963 
7964         private CharSequence mBeforeText;
7965 
7966         private EasyEditSpanController mEasyEditSpanController;
7967 
ChangeWatcher()7968         private ChangeWatcher() {
7969             mEasyEditSpanController = new EasyEditSpanController();
7970         }
7971 
beforeTextChanged(CharSequence buffer, int start, int before, int after)7972         public void beforeTextChanged(CharSequence buffer, int start,
7973                                       int before, int after) {
7974             if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
7975                     + " before=" + before + " after=" + after + ": " + buffer);
7976 
7977             if (AccessibilityManager.getInstance(mContext).isEnabled()
7978                     && !isPasswordInputType(mInputType)
7979                     && !hasPasswordTransformationMethod()) {
7980                 mBeforeText = buffer.toString();
7981             }
7982 
7983             TextView.this.sendBeforeTextChanged(buffer, start, before, after);
7984         }
7985 
onTextChanged(CharSequence buffer, int start, int before, int after)7986         public void onTextChanged(CharSequence buffer, int start,
7987                                   int before, int after) {
7988             if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
7989                     + " before=" + before + " after=" + after + ": " + buffer);
7990             TextView.this.handleTextChanged(buffer, start, before, after);
7991 
7992             mEasyEditSpanController.onTextChange(buffer);
7993 
7994             if (AccessibilityManager.getInstance(mContext).isEnabled() &&
7995                     (isFocused() || isSelected() && isShown())) {
7996                 sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
7997                 mBeforeText = null;
7998             }
7999         }
8000 
afterTextChanged(Editable buffer)8001         public void afterTextChanged(Editable buffer) {
8002             if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
8003             TextView.this.sendAfterTextChanged(buffer);
8004 
8005             if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
8006                 MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
8007             }
8008         }
8009 
onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en)8010         public void onSpanChanged(Spannable buf,
8011                                   Object what, int s, int e, int st, int en) {
8012             if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
8013                     + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
8014             TextView.this.spanChange(buf, what, s, st, e, en);
8015         }
8016 
onSpanAdded(Spannable buf, Object what, int s, int e)8017         public void onSpanAdded(Spannable buf, Object what, int s, int e) {
8018             if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
8019                     + " what=" + what + ": " + buf);
8020             TextView.this.spanChange(buf, what, -1, s, -1, e);
8021         }
8022 
onSpanRemoved(Spannable buf, Object what, int s, int e)8023         public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
8024             if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
8025                     + " what=" + what + ": " + buf);
8026             TextView.this.spanChange(buf, what, s, -1, e, -1);
8027         }
8028 
hideControllers()8029         private void hideControllers() {
8030             mEasyEditSpanController.hide();
8031         }
8032     }
8033 
8034     /**
8035      * @hide
8036      */
8037     @Override
dispatchFinishTemporaryDetach()8038     public void dispatchFinishTemporaryDetach() {
8039         mDispatchTemporaryDetach = true;
8040         super.dispatchFinishTemporaryDetach();
8041         mDispatchTemporaryDetach = false;
8042     }
8043 
8044     @Override
onStartTemporaryDetach()8045     public void onStartTemporaryDetach() {
8046         super.onStartTemporaryDetach();
8047         // Only track when onStartTemporaryDetach() is called directly,
8048         // usually because this instance is an editable field in a list
8049         if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
8050 
8051         // Because of View recycling in ListView, there is no easy way to know when a TextView with
8052         // selection becomes visible again. Until a better solution is found, stop text selection
8053         // mode (if any) as soon as this TextView is recycled.
8054         hideControllers();
8055     }
8056 
8057     @Override
onFinishTemporaryDetach()8058     public void onFinishTemporaryDetach() {
8059         super.onFinishTemporaryDetach();
8060         // Only track when onStartTemporaryDetach() is called directly,
8061         // usually because this instance is an editable field in a list
8062         if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
8063     }
8064 
8065     @Override
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)8066     protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
8067         if (mTemporaryDetach) {
8068             // If we are temporarily in the detach state, then do nothing.
8069             super.onFocusChanged(focused, direction, previouslyFocusedRect);
8070             return;
8071         }
8072 
8073         mShowCursor = SystemClock.uptimeMillis();
8074 
8075         ensureEndedBatchEdit();
8076 
8077         if (focused) {
8078             int selStart = getSelectionStart();
8079             int selEnd = getSelectionEnd();
8080 
8081             // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
8082             // mode for these, unless there was a specific selection already started.
8083             final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
8084                     selEnd == mText.length();
8085             mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
8086 
8087             if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
8088                 // If a tap was used to give focus to that view, move cursor at tap position.
8089                 // Has to be done before onTakeFocus, which can be overloaded.
8090                 final int lastTapPosition = getLastTapPosition();
8091                 if (lastTapPosition >= 0) {
8092                     Selection.setSelection((Spannable) mText, lastTapPosition);
8093                 }
8094 
8095                 if (mMovement != null) {
8096                     mMovement.onTakeFocus(this, (Spannable) mText, direction);
8097                 }
8098 
8099                 // The DecorView does not have focus when the 'Done' ExtractEditText button is
8100                 // pressed. Since it is the ViewAncestor's mView, it requests focus before
8101                 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
8102                 // This special case ensure that we keep current selection in that case.
8103                 // It would be better to know why the DecorView does not have focus at that time.
8104                 if (((this instanceof ExtractEditText) || mSelectionMoved) &&
8105                         selStart >= 0 && selEnd >= 0) {
8106                     /*
8107                      * Someone intentionally set the selection, so let them
8108                      * do whatever it is that they wanted to do instead of
8109                      * the default on-focus behavior.  We reset the selection
8110                      * here instead of just skipping the onTakeFocus() call
8111                      * because some movement methods do something other than
8112                      * just setting the selection in theirs and we still
8113                      * need to go through that path.
8114                      */
8115                     Selection.setSelection((Spannable) mText, selStart, selEnd);
8116                 }
8117 
8118                 if (mSelectAllOnFocus) {
8119                     selectAll();
8120                 }
8121 
8122                 mTouchFocusSelected = true;
8123             }
8124 
8125             mFrozenWithFocus = false;
8126             mSelectionMoved = false;
8127 
8128             if (mText instanceof Spannable) {
8129                 Spannable sp = (Spannable) mText;
8130                 MetaKeyKeyListener.resetMetaState(sp);
8131             }
8132 
8133             makeBlink();
8134 
8135             if (mError != null) {
8136                 showError();
8137             }
8138         } else {
8139             if (mError != null) {
8140                 hideError();
8141             }
8142             // Don't leave us in the middle of a batch edit.
8143             onEndBatchEdit();
8144 
8145             if (this instanceof ExtractEditText) {
8146                 // terminateTextSelectionMode removes selection, which we want to keep when
8147                 // ExtractEditText goes out of focus.
8148                 final int selStart = getSelectionStart();
8149                 final int selEnd = getSelectionEnd();
8150                 hideControllers();
8151                 Selection.setSelection((Spannable) mText, selStart, selEnd);
8152             } else {
8153                 hideControllers();
8154                 downgradeEasyCorrectionSpans();
8155             }
8156 
8157             // No need to create the controller
8158             if (mSelectionModifierCursorController != null) {
8159                 mSelectionModifierCursorController.resetTouchOffsets();
8160             }
8161         }
8162 
8163         startStopMarquee(focused);
8164 
8165         if (mTransformation != null) {
8166             mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
8167         }
8168 
8169         super.onFocusChanged(focused, direction, previouslyFocusedRect);
8170     }
8171 
getLastTapPosition()8172     private int getLastTapPosition() {
8173         // No need to create the controller at that point, no last tap position saved
8174         if (mSelectionModifierCursorController != null) {
8175             int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
8176             if (lastTapPosition >= 0) {
8177                 // Safety check, should not be possible.
8178                 if (lastTapPosition > mText.length()) {
8179                     Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
8180                             + mText.length() + ")");
8181                     lastTapPosition = mText.length();
8182                 }
8183                 return lastTapPosition;
8184             }
8185         }
8186 
8187         return -1;
8188     }
8189 
8190     @Override
onWindowFocusChanged(boolean hasWindowFocus)8191     public void onWindowFocusChanged(boolean hasWindowFocus) {
8192         super.onWindowFocusChanged(hasWindowFocus);
8193 
8194         if (hasWindowFocus) {
8195             if (mBlink != null) {
8196                 mBlink.uncancel();
8197                 makeBlink();
8198             }
8199         } else {
8200             if (mBlink != null) {
8201                 mBlink.cancel();
8202             }
8203             // Don't leave us in the middle of a batch edit.
8204             onEndBatchEdit();
8205             if (mInputContentType != null) {
8206                 mInputContentType.enterDown = false;
8207             }
8208 
8209             hideControllers();
8210         }
8211 
8212         startStopMarquee(hasWindowFocus);
8213     }
8214 
8215     @Override
onVisibilityChanged(View changedView, int visibility)8216     protected void onVisibilityChanged(View changedView, int visibility) {
8217         super.onVisibilityChanged(changedView, visibility);
8218         if (visibility != VISIBLE) {
8219             hideControllers();
8220         }
8221     }
8222 
8223     /**
8224      * Use {@link BaseInputConnection#removeComposingSpans
8225      * BaseInputConnection.removeComposingSpans()} to remove any IME composing
8226      * state from this text view.
8227      */
clearComposingText()8228     public void clearComposingText() {
8229         if (mText instanceof Spannable) {
8230             BaseInputConnection.removeComposingSpans((Spannable)mText);
8231         }
8232     }
8233 
8234     @Override
setSelected(boolean selected)8235     public void setSelected(boolean selected) {
8236         boolean wasSelected = isSelected();
8237 
8238         super.setSelected(selected);
8239 
8240         if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
8241             if (selected) {
8242                 startMarquee();
8243             } else {
8244                 stopMarquee();
8245             }
8246         }
8247     }
8248 
8249     @Override
onTouchEvent(MotionEvent event)8250     public boolean onTouchEvent(MotionEvent event) {
8251         final int action = event.getActionMasked();
8252 
8253         if (hasSelectionController()) {
8254             getSelectionController().onTouchEvent(event);
8255         }
8256 
8257         if (action == MotionEvent.ACTION_DOWN) {
8258             mLastDownPositionX = event.getX();
8259             mLastDownPositionY = event.getY();
8260 
8261             // Reset this state; it will be re-set if super.onTouchEvent
8262             // causes focus to move to the view.
8263             mTouchFocusSelected = false;
8264             mIgnoreActionUpEvent = false;
8265         }
8266 
8267         final boolean superResult = super.onTouchEvent(event);
8268 
8269         /*
8270          * Don't handle the release after a long press, because it will
8271          * move the selection away from whatever the menu action was
8272          * trying to affect.
8273          */
8274         if (mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
8275             mDiscardNextActionUp = false;
8276             return superResult;
8277         }
8278 
8279         final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
8280                 !shouldIgnoreActionUpEvent() && isFocused();
8281 
8282          if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
8283                 && mText instanceof Spannable && mLayout != null) {
8284             boolean handled = false;
8285 
8286             if (mMovement != null) {
8287                 handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
8288             }
8289 
8290             if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && mTextIsSelectable) {
8291                 // The LinkMovementMethod which should handle taps on links has not been installed
8292                 // on non editable text that support text selection.
8293                 // We reproduce its behavior here to open links for these.
8294                 ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
8295                         getSelectionEnd(), ClickableSpan.class);
8296 
8297                 if (links.length != 0) {
8298                     links[0].onClick(this);
8299                     handled = true;
8300                 }
8301             }
8302 
8303             if (touchIsFinished && (isTextEditable() || mTextIsSelectable)) {
8304                 // Show the IME, except when selecting in read-only text.
8305                 final InputMethodManager imm = InputMethodManager.peekInstance();
8306                 viewClicked(imm);
8307                 if (!mTextIsSelectable) {
8308                     handled |= imm != null && imm.showSoftInput(this, 0);
8309                 }
8310 
8311                 boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
8312                 hideControllers();
8313                 if (!selectAllGotFocus && mText.length() > 0) {
8314                     if (mSpellChecker != null) {
8315                         // When the cursor moves, the word that was typed may need spell check
8316                         mSpellChecker.onSelectionChanged();
8317                     }
8318                     if (isCursorInsideEasyCorrectionSpan()) {
8319                         showSuggestions();
8320                     } else if (hasInsertionController()) {
8321                         getInsertionController().show();
8322                     }
8323                 }
8324 
8325                 handled = true;
8326             }
8327 
8328             if (handled) {
8329                 return true;
8330             }
8331         }
8332 
8333         return superResult;
8334     }
8335 
8336     /**
8337      * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
8338      */
isCursorInsideSuggestionSpan()8339     private boolean isCursorInsideSuggestionSpan() {
8340         if (!(mText instanceof Spannable)) return false;
8341 
8342         SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(),
8343                 getSelectionEnd(), SuggestionSpan.class);
8344         return (suggestionSpans.length > 0);
8345     }
8346 
8347     /**
8348      * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
8349      * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
8350      */
isCursorInsideEasyCorrectionSpan()8351     private boolean isCursorInsideEasyCorrectionSpan() {
8352         Spannable spannable = (Spannable) mText;
8353         SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(),
8354                 getSelectionEnd(), SuggestionSpan.class);
8355         for (int i = 0; i < suggestionSpans.length; i++) {
8356             if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
8357                 return true;
8358             }
8359         }
8360         return false;
8361     }
8362 
8363     /**
8364      * Downgrades to simple suggestions all the easy correction spans that are not a spell check
8365      * span.
8366      */
downgradeEasyCorrectionSpans()8367     private void downgradeEasyCorrectionSpans() {
8368         if (mText instanceof Spannable) {
8369             Spannable spannable = (Spannable) mText;
8370             SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
8371                     spannable.length(), SuggestionSpan.class);
8372             for (int i = 0; i < suggestionSpans.length; i++) {
8373                 int flags = suggestionSpans[i].getFlags();
8374                 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
8375                         && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
8376                     flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
8377                     suggestionSpans[i].setFlags(flags);
8378                 }
8379             }
8380         }
8381     }
8382 
8383     @Override
onGenericMotionEvent(MotionEvent event)8384     public boolean onGenericMotionEvent(MotionEvent event) {
8385         if (mMovement != null && mText instanceof Spannable && mLayout != null) {
8386             try {
8387                 if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
8388                     return true;
8389                 }
8390             } catch (AbstractMethodError ex) {
8391                 // onGenericMotionEvent was added to the MovementMethod interface in API 12.
8392                 // Ignore its absence in case third party applications implemented the
8393                 // interface directly.
8394             }
8395         }
8396         return super.onGenericMotionEvent(event);
8397     }
8398 
prepareCursorControllers()8399     private void prepareCursorControllers() {
8400         boolean windowSupportsHandles = false;
8401 
8402         ViewGroup.LayoutParams params = getRootView().getLayoutParams();
8403         if (params instanceof WindowManager.LayoutParams) {
8404             WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
8405             windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
8406                     || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
8407         }
8408 
8409         mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
8410         mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
8411                 mLayout != null;
8412 
8413         if (!mInsertionControllerEnabled) {
8414             hideInsertionPointCursorController();
8415             if (mInsertionPointCursorController != null) {
8416                 mInsertionPointCursorController.onDetached();
8417                 mInsertionPointCursorController = null;
8418             }
8419         }
8420 
8421         if (!mSelectionControllerEnabled) {
8422             stopSelectionActionMode();
8423             if (mSelectionModifierCursorController != null) {
8424                 mSelectionModifierCursorController.onDetached();
8425                 mSelectionModifierCursorController = null;
8426             }
8427         }
8428     }
8429 
8430     /**
8431      * @return True iff this TextView contains a text that can be edited, or if this is
8432      * a selectable TextView.
8433      */
isTextEditable()8434     private boolean isTextEditable() {
8435         return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
8436     }
8437 
8438     /**
8439      * Returns true, only while processing a touch gesture, if the initial
8440      * touch down event caused focus to move to the text view and as a result
8441      * its selection changed.  Only valid while processing the touch gesture
8442      * of interest.
8443      */
didTouchFocusSelect()8444     public boolean didTouchFocusSelect() {
8445         return mTouchFocusSelected;
8446     }
8447 
8448     @Override
cancelLongPress()8449     public void cancelLongPress() {
8450         super.cancelLongPress();
8451         mIgnoreActionUpEvent = true;
8452     }
8453 
8454     /**
8455      * This method is only valid during a touch event.
8456      *
8457      * @return true when the ACTION_UP event should be ignored, false otherwise.
8458      *
8459      * @hide
8460      */
shouldIgnoreActionUpEvent()8461     public boolean shouldIgnoreActionUpEvent() {
8462         return mIgnoreActionUpEvent;
8463     }
8464 
8465     @Override
onTrackballEvent(MotionEvent event)8466     public boolean onTrackballEvent(MotionEvent event) {
8467         if (mMovement != null && mText instanceof Spannable &&
8468             mLayout != null) {
8469             if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
8470                 return true;
8471             }
8472         }
8473 
8474         return super.onTrackballEvent(event);
8475     }
8476 
setScroller(Scroller s)8477     public void setScroller(Scroller s) {
8478         mScroller = s;
8479     }
8480 
8481     private static class Blink extends Handler implements Runnable {
8482         private final WeakReference<TextView> mView;
8483         private boolean mCancelled;
8484 
Blink(TextView v)8485         public Blink(TextView v) {
8486             mView = new WeakReference<TextView>(v);
8487         }
8488 
run()8489         public void run() {
8490             if (mCancelled) {
8491                 return;
8492             }
8493 
8494             removeCallbacks(Blink.this);
8495 
8496             TextView tv = mView.get();
8497 
8498             if (tv != null && tv.shouldBlink()) {
8499                 if (tv.mLayout != null) {
8500                     tv.invalidateCursorPath();
8501                 }
8502 
8503                 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
8504             }
8505         }
8506 
cancel()8507         void cancel() {
8508             if (!mCancelled) {
8509                 removeCallbacks(Blink.this);
8510                 mCancelled = true;
8511             }
8512         }
8513 
uncancel()8514         void uncancel() {
8515             mCancelled = false;
8516         }
8517     }
8518 
8519     /**
8520      * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
8521      */
shouldBlink()8522     private boolean shouldBlink() {
8523         if (!isFocused()) return false;
8524 
8525         final int start = getSelectionStart();
8526         if (start < 0) return false;
8527 
8528         final int end = getSelectionEnd();
8529         if (end < 0) return false;
8530 
8531         return start == end;
8532     }
8533 
makeBlink()8534     private void makeBlink() {
8535         if (isCursorVisible()) {
8536             if (shouldBlink()) {
8537                 mShowCursor = SystemClock.uptimeMillis();
8538                 if (mBlink == null) mBlink = new Blink(this);
8539                 mBlink.removeCallbacks(mBlink);
8540                 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
8541             }
8542         } else {
8543             if (mBlink != null) mBlink.removeCallbacks(mBlink);
8544         }
8545     }
8546 
8547     @Override
getLeftFadingEdgeStrength()8548     protected float getLeftFadingEdgeStrength() {
8549         if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
8550         if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8551                 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
8552             if (mMarquee != null && !mMarquee.isStopped()) {
8553                 final Marquee marquee = mMarquee;
8554                 if (marquee.shouldDrawLeftFade()) {
8555                     return marquee.mScroll / getHorizontalFadingEdgeLength();
8556                 } else {
8557                     return 0.0f;
8558                 }
8559             } else if (getLineCount() == 1) {
8560                 final int layoutDirection = getResolvedLayoutDirection();
8561                 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
8562                 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
8563                     case Gravity.LEFT:
8564                         return 0.0f;
8565                     case Gravity.RIGHT:
8566                         return (mLayout.getLineRight(0) - (mRight - mLeft) -
8567                                 getCompoundPaddingLeft() - getCompoundPaddingRight() -
8568                                 mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
8569                     case Gravity.CENTER_HORIZONTAL:
8570                         return 0.0f;
8571                 }
8572             }
8573         }
8574         return super.getLeftFadingEdgeStrength();
8575     }
8576 
8577     @Override
getRightFadingEdgeStrength()8578     protected float getRightFadingEdgeStrength() {
8579         if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
8580         if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8581                 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
8582             if (mMarquee != null && !mMarquee.isStopped()) {
8583                 final Marquee marquee = mMarquee;
8584                 return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
8585             } else if (getLineCount() == 1) {
8586                 final int layoutDirection = getResolvedLayoutDirection();
8587                 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
8588                 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
8589                     case Gravity.LEFT:
8590                         final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
8591                                 getCompoundPaddingRight();
8592                         final float lineWidth = mLayout.getLineWidth(0);
8593                         return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
8594                     case Gravity.RIGHT:
8595                         return 0.0f;
8596                     case Gravity.CENTER_HORIZONTAL:
8597                     case Gravity.FILL_HORIZONTAL:
8598                         return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
8599                                 getCompoundPaddingLeft() - getCompoundPaddingRight())) /
8600                                 getHorizontalFadingEdgeLength();
8601                 }
8602             }
8603         }
8604         return super.getRightFadingEdgeStrength();
8605     }
8606 
8607     @Override
computeHorizontalScrollRange()8608     protected int computeHorizontalScrollRange() {
8609         if (mLayout != null) {
8610             return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
8611                     (int) mLayout.getLineWidth(0) : mLayout.getWidth();
8612         }
8613 
8614         return super.computeHorizontalScrollRange();
8615     }
8616 
8617     @Override
computeVerticalScrollRange()8618     protected int computeVerticalScrollRange() {
8619         if (mLayout != null)
8620             return mLayout.getHeight();
8621 
8622         return super.computeVerticalScrollRange();
8623     }
8624 
8625     @Override
computeVerticalScrollExtent()8626     protected int computeVerticalScrollExtent() {
8627         return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
8628     }
8629 
8630     @Override
findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags)8631     public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
8632         super.findViewsWithText(outViews, searched, flags);
8633         if (!outViews.contains(this) && (flags & FIND_VIEWS_WITH_TEXT) != 0
8634                 && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mText)) {
8635             String searchedLowerCase = searched.toString().toLowerCase();
8636             String textLowerCase = mText.toString().toLowerCase();
8637             if (textLowerCase.contains(searchedLowerCase)) {
8638                 outViews.add(this);
8639             }
8640         }
8641     }
8642 
8643     public enum BufferType {
8644         NORMAL, SPANNABLE, EDITABLE,
8645     }
8646 
8647     /**
8648      * Returns the TextView_textColor attribute from the
8649      * Resources.StyledAttributes, if set, or the TextAppearance_textColor
8650      * from the TextView_textAppearance attribute, if TextView_textColor
8651      * was not set directly.
8652      */
getTextColors(Context context, TypedArray attrs)8653     public static ColorStateList getTextColors(Context context, TypedArray attrs) {
8654         ColorStateList colors;
8655         colors = attrs.getColorStateList(com.android.internal.R.styleable.
8656                                          TextView_textColor);
8657 
8658         if (colors == null) {
8659             int ap = attrs.getResourceId(com.android.internal.R.styleable.
8660                                          TextView_textAppearance, -1);
8661             if (ap != -1) {
8662                 TypedArray appearance;
8663                 appearance = context.obtainStyledAttributes(ap,
8664                                             com.android.internal.R.styleable.TextAppearance);
8665                 colors = appearance.getColorStateList(com.android.internal.R.styleable.
8666                                                   TextAppearance_textColor);
8667                 appearance.recycle();
8668             }
8669         }
8670 
8671         return colors;
8672     }
8673 
8674     /**
8675      * Returns the default color from the TextView_textColor attribute
8676      * from the AttributeSet, if set, or the default color from the
8677      * TextAppearance_textColor from the TextView_textAppearance attribute,
8678      * if TextView_textColor was not set directly.
8679      */
getTextColor(Context context, TypedArray attrs, int def)8680     public static int getTextColor(Context context,
8681                                    TypedArray attrs,
8682                                    int def) {
8683         ColorStateList colors = getTextColors(context, attrs);
8684 
8685         if (colors == null) {
8686             return def;
8687         } else {
8688             return colors.getDefaultColor();
8689         }
8690     }
8691 
8692     @Override
onKeyShortcut(int keyCode, KeyEvent event)8693     public boolean onKeyShortcut(int keyCode, KeyEvent event) {
8694         final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK;
8695         if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) {
8696             switch (keyCode) {
8697             case KeyEvent.KEYCODE_A:
8698                 if (canSelectText()) {
8699                     return onTextContextMenuItem(ID_SELECT_ALL);
8700                 }
8701                 break;
8702             case KeyEvent.KEYCODE_X:
8703                 if (canCut()) {
8704                     return onTextContextMenuItem(ID_CUT);
8705                 }
8706                 break;
8707             case KeyEvent.KEYCODE_C:
8708                 if (canCopy()) {
8709                     return onTextContextMenuItem(ID_COPY);
8710                 }
8711                 break;
8712             case KeyEvent.KEYCODE_V:
8713                 if (canPaste()) {
8714                     return onTextContextMenuItem(ID_PASTE);
8715                 }
8716                 break;
8717             }
8718         }
8719         return super.onKeyShortcut(keyCode, event);
8720     }
8721 
8722     /**
8723      * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
8724      * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
8725      * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
8726      */
canSelectText()8727     private boolean canSelectText() {
8728         return hasSelectionController() && mText.length() != 0;
8729     }
8730 
8731     /**
8732      * Test based on the <i>intrinsic</i> charateristics of the TextView.
8733      * The text must be spannable and the movement method must allow for arbitary selection.
8734      *
8735      * See also {@link #canSelectText()}.
8736      */
textCanBeSelected()8737     private boolean textCanBeSelected() {
8738         // prepareCursorController() relies on this method.
8739         // If you change this condition, make sure prepareCursorController is called anywhere
8740         // the value of this condition might be changed.
8741         if (mMovement == null || !mMovement.canSelectArbitrarily()) return false;
8742         return isTextEditable() || (mTextIsSelectable && mText instanceof Spannable && isEnabled());
8743     }
8744 
canCut()8745     private boolean canCut() {
8746         if (hasPasswordTransformationMethod()) {
8747             return false;
8748         }
8749 
8750         if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mInput != null) {
8751             return true;
8752         }
8753 
8754         return false;
8755     }
8756 
canCopy()8757     private boolean canCopy() {
8758         if (hasPasswordTransformationMethod()) {
8759             return false;
8760         }
8761 
8762         if (mText.length() > 0 && hasSelection()) {
8763             return true;
8764         }
8765 
8766         return false;
8767     }
8768 
canPaste()8769     private boolean canPaste() {
8770         return (mText instanceof Editable &&
8771                 mInput != null &&
8772                 getSelectionStart() >= 0 &&
8773                 getSelectionEnd() >= 0 &&
8774                 ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
8775                 hasPrimaryClip());
8776     }
8777 
packRangeInLong(int start, int end)8778     private static long packRangeInLong(int start, int end) {
8779         return (((long) start) << 32) | end;
8780     }
8781 
extractRangeStartFromLong(long range)8782     private static int extractRangeStartFromLong(long range) {
8783         return (int) (range >>> 32);
8784     }
8785 
extractRangeEndFromLong(long range)8786     private static int extractRangeEndFromLong(long range) {
8787         return (int) (range & 0x00000000FFFFFFFFL);
8788     }
8789 
selectAll()8790     private boolean selectAll() {
8791         final int length = mText.length();
8792         Selection.setSelection((Spannable) mText, 0, length);
8793         return length > 0;
8794     }
8795 
8796     /**
8797      * Adjusts selection to the word under last touch offset.
8798      * Return true if the operation was successfully performed.
8799      */
selectCurrentWord()8800     private boolean selectCurrentWord() {
8801         if (!canSelectText()) {
8802             return false;
8803         }
8804 
8805         if (hasPasswordTransformationMethod()) {
8806             // Always select all on a password field.
8807             // Cut/copy menu entries are not available for passwords, but being able to select all
8808             // is however useful to delete or paste to replace the entire content.
8809             return selectAll();
8810         }
8811 
8812         int klass = mInputType & InputType.TYPE_MASK_CLASS;
8813         int variation = mInputType & InputType.TYPE_MASK_VARIATION;
8814 
8815         // Specific text field types: select the entire text for these
8816         if (klass == InputType.TYPE_CLASS_NUMBER ||
8817                 klass == InputType.TYPE_CLASS_PHONE ||
8818                 klass == InputType.TYPE_CLASS_DATETIME ||
8819                 variation == InputType.TYPE_TEXT_VARIATION_URI ||
8820                 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
8821                 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
8822                 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
8823             return selectAll();
8824         }
8825 
8826         long lastTouchOffsets = getLastTouchOffsets();
8827         final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
8828         final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
8829 
8830         // Safety check in case standard touch event handling has been bypassed
8831         if (minOffset < 0 || minOffset >= mText.length()) return false;
8832         if (maxOffset < 0 || maxOffset >= mText.length()) return false;
8833 
8834         int selectionStart, selectionEnd;
8835 
8836         // If a URLSpan (web address, email, phone...) is found at that position, select it.
8837         URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
8838         if (urlSpans.length >= 1) {
8839             URLSpan urlSpan = urlSpans[0];
8840             selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
8841             selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
8842         } else {
8843             if (mWordIterator == null) {
8844                 mWordIterator = new WordIterator();
8845             }
8846             mWordIterator.setCharSequence(mText, minOffset, maxOffset);
8847 
8848             selectionStart = mWordIterator.getBeginning(minOffset);
8849             if (selectionStart == BreakIterator.DONE) return false;
8850 
8851             selectionEnd = mWordIterator.getEnd(maxOffset);
8852             if (selectionEnd == BreakIterator.DONE) return false;
8853 
8854             if (selectionStart == selectionEnd) {
8855                 // Possible when the word iterator does not properly handle the text's language
8856                 long range = getCharRange(selectionStart);
8857                 selectionStart = extractRangeStartFromLong(range);
8858                 selectionEnd = extractRangeEndFromLong(range);
8859             }
8860         }
8861 
8862         Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
8863         return selectionEnd > selectionStart;
8864     }
8865 
getCharRange(int offset)8866     private long getCharRange(int offset) {
8867         final int textLength = mText.length();
8868         if (offset + 1 < textLength) {
8869             final char currentChar = mText.charAt(offset);
8870             final char nextChar = mText.charAt(offset + 1);
8871             if (Character.isSurrogatePair(currentChar, nextChar)) {
8872                 return packRangeInLong(offset,  offset + 2);
8873             }
8874         }
8875         if (offset < textLength) {
8876             return packRangeInLong(offset,  offset + 1);
8877         }
8878         if (offset - 2 >= 0) {
8879             final char previousChar = mText.charAt(offset - 1);
8880             final char previousPreviousChar = mText.charAt(offset - 2);
8881             if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
8882                 return packRangeInLong(offset - 2,  offset);
8883             }
8884         }
8885         if (offset - 1 >= 0) {
8886             return packRangeInLong(offset - 1,  offset);
8887         }
8888         return packRangeInLong(offset,  offset);
8889     }
8890 
getSpellChecker()8891     private SpellChecker getSpellChecker() {
8892         if (mSpellChecker == null) {
8893             mSpellChecker = new SpellChecker(this);
8894         }
8895         return mSpellChecker;
8896     }
8897 
getLastTouchOffsets()8898     private long getLastTouchOffsets() {
8899         int minOffset, maxOffset;
8900 
8901         if (mContextMenuTriggeredByKey) {
8902             minOffset = getSelectionStart();
8903             maxOffset = getSelectionEnd();
8904         } else {
8905             SelectionModifierCursorController selectionController = getSelectionController();
8906             minOffset = selectionController.getMinTouchOffset();
8907             maxOffset = selectionController.getMaxTouchOffset();
8908         }
8909 
8910         return packRangeInLong(minOffset, maxOffset);
8911     }
8912 
8913     @Override
onPopulateAccessibilityEvent(AccessibilityEvent event)8914     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
8915         super.onPopulateAccessibilityEvent(event);
8916 
8917         final boolean isPassword = hasPasswordTransformationMethod();
8918         if (!isPassword) {
8919             CharSequence text = getTextForAccessibility();
8920             if (!TextUtils.isEmpty(text)) {
8921                 event.getText().add(text);
8922             }
8923         }
8924     }
8925 
8926     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)8927     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
8928         super.onInitializeAccessibilityEvent(event);
8929 
8930         final boolean isPassword = hasPasswordTransformationMethod();
8931         event.setPassword(isPassword);
8932 
8933         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
8934             event.setFromIndex(Selection.getSelectionStart(mText));
8935             event.setToIndex(Selection.getSelectionEnd(mText));
8936             event.setItemCount(mText.length());
8937         }
8938     }
8939 
8940     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)8941     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
8942         super.onInitializeAccessibilityNodeInfo(info);
8943 
8944         final boolean isPassword = hasPasswordTransformationMethod();
8945         if (!isPassword) {
8946             info.setText(getTextForAccessibility());
8947         }
8948         info.setPassword(isPassword);
8949     }
8950 
8951     @Override
sendAccessibilityEvent(int eventType)8952     public void sendAccessibilityEvent(int eventType) {
8953         // Do not send scroll events since first they are not interesting for
8954         // accessibility and second such events a generated too frequently.
8955         // For details see the implementation of bringTextIntoView().
8956         if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
8957             return;
8958         }
8959         super.sendAccessibilityEvent(eventType);
8960     }
8961 
8962     /**
8963      * Gets the text reported for accessibility purposes. It is the
8964      * text if not empty or the hint.
8965      *
8966      * @return The accessibility text.
8967      */
getTextForAccessibility()8968     private CharSequence getTextForAccessibility() {
8969         CharSequence text = getText();
8970         if (TextUtils.isEmpty(text)) {
8971             text = getHint();
8972         }
8973         return text;
8974     }
8975 
sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, int removedCount, int addedCount)8976     void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
8977             int fromIndex, int removedCount, int addedCount) {
8978         AccessibilityEvent event =
8979             AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
8980         event.setFromIndex(fromIndex);
8981         event.setRemovedCount(removedCount);
8982         event.setAddedCount(addedCount);
8983         event.setBeforeText(beforeText);
8984         sendAccessibilityEventUnchecked(event);
8985     }
8986 
8987     @Override
onCreateContextMenu(ContextMenu menu)8988     protected void onCreateContextMenu(ContextMenu menu) {
8989         super.onCreateContextMenu(menu);
8990         boolean added = false;
8991         mContextMenuTriggeredByKey = mDPadCenterIsDown || mEnterKeyIsDown;
8992         // Problem with context menu on long press: the menu appears while the key in down and when
8993         // the key is released, the view does not receive the key_up event.
8994         // We need two layers of flags: mDPadCenterIsDown and mEnterKeyIsDown are set in key down/up
8995         // events. We cannot simply clear these flags in onTextContextMenuItem since
8996         // it may not be called (if the user/ discards the context menu with the back key).
8997         // We clear these flags here and mContextMenuTriggeredByKey saves that state so that it is
8998         // available in onTextContextMenuItem.
8999         mDPadCenterIsDown = mEnterKeyIsDown = false;
9000 
9001         MenuHandler handler = new MenuHandler();
9002 
9003         if (mText instanceof Spanned && hasSelectionController()) {
9004             long lastTouchOffset = getLastTouchOffsets();
9005             final int selStart = extractRangeStartFromLong(lastTouchOffset);
9006             final int selEnd = extractRangeEndFromLong(lastTouchOffset);
9007 
9008             URLSpan[] urls = ((Spanned) mText).getSpans(selStart, selEnd, URLSpan.class);
9009             if (urls.length > 0) {
9010                 menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl).
9011                         setOnMenuItemClickListener(handler);
9012 
9013                 added = true;
9014             }
9015         }
9016 
9017         // The context menu is not empty, which will prevent the selection mode from starting.
9018         // Add a entry to start it in the context menu.
9019         // TODO Does not handle the case where a subclass does not call super.thisMethod or
9020         // populates the menu AFTER this call.
9021         if (menu.size() > 0) {
9022             menu.add(0, ID_SELECTION_MODE, 0, com.android.internal.R.string.selectTextMode).
9023                     setOnMenuItemClickListener(handler);
9024             added = true;
9025         }
9026 
9027         if (added) {
9028             menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
9029         }
9030     }
9031 
9032     /**
9033      * Returns whether this text view is a current input method target.  The
9034      * default implementation just checks with {@link InputMethodManager}.
9035      */
isInputMethodTarget()9036     public boolean isInputMethodTarget() {
9037         InputMethodManager imm = InputMethodManager.peekInstance();
9038         return imm != null && imm.isActive(this);
9039     }
9040 
9041     // Selection context mode
9042     private static final int ID_SELECT_ALL = android.R.id.selectAll;
9043     private static final int ID_CUT = android.R.id.cut;
9044     private static final int ID_COPY = android.R.id.copy;
9045     private static final int ID_PASTE = android.R.id.paste;
9046     // Context menu entries
9047     private static final int ID_COPY_URL = android.R.id.copyUrl;
9048     private static final int ID_SELECTION_MODE = android.R.id.selectTextMode;
9049 
9050     private class MenuHandler implements MenuItem.OnMenuItemClickListener {
onMenuItemClick(MenuItem item)9051         public boolean onMenuItemClick(MenuItem item) {
9052             return onTextContextMenuItem(item.getItemId());
9053         }
9054     }
9055 
9056     /**
9057      * Called when a context menu option for the text view is selected.  Currently
9058      * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode},
9059      * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut}
9060      * or {@link android.R.id#copy}.
9061      *
9062      * @return true if the context menu item action was performed.
9063      */
onTextContextMenuItem(int id)9064     public boolean onTextContextMenuItem(int id) {
9065         int min = 0;
9066         int max = mText.length();
9067 
9068         if (isFocused()) {
9069             final int selStart = getSelectionStart();
9070             final int selEnd = getSelectionEnd();
9071 
9072             min = Math.max(0, Math.min(selStart, selEnd));
9073             max = Math.max(0, Math.max(selStart, selEnd));
9074         }
9075 
9076         switch (id) {
9077             case ID_COPY_URL:
9078                 URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class);
9079                 if (urls.length >= 1) {
9080                     ClipData clip = null;
9081                     for (int i=0; i<urls.length; i++) {
9082                         Uri uri = Uri.parse(urls[0].getURL());
9083                         if (clip == null) {
9084                             clip = ClipData.newRawUri(null, uri);
9085                         } else {
9086                             clip.addItem(new ClipData.Item(uri));
9087                         }
9088                     }
9089                     if (clip != null) {
9090                         setPrimaryClip(clip);
9091                     }
9092                 }
9093                 stopSelectionActionMode();
9094                 return true;
9095 
9096             case ID_SELECTION_MODE:
9097                 if (mSelectionActionMode != null) {
9098                     // Selection mode is already started, simply change selected part.
9099                     selectCurrentWord();
9100                 } else {
9101                     startSelectionActionMode();
9102                 }
9103                 return true;
9104 
9105             case ID_SELECT_ALL:
9106                 // This does not enter text selection mode. Text is highlighted, so that it can be
9107                 // bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
9108                 selectAll();
9109                 return true;
9110 
9111             case ID_PASTE:
9112                 paste(min, max);
9113                 return true;
9114 
9115             case ID_CUT:
9116                 setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
9117                 ((Editable) mText).delete(min, max);
9118                 stopSelectionActionMode();
9119                 return true;
9120 
9121             case ID_COPY:
9122                 setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
9123                 stopSelectionActionMode();
9124                 return true;
9125         }
9126         return false;
9127     }
9128 
getTransformedText(int start, int end)9129     private CharSequence getTransformedText(int start, int end) {
9130         return removeSuggestionSpans(mTransformed.subSequence(start, end));
9131     }
9132 
9133     /**
9134      * Prepare text so that there are not zero or two spaces at beginning and end of region defined
9135      * by [min, max] when replacing this region by paste.
9136      * Note that if there were two spaces (or more) at that position before, they are kept. We just
9137      * make sure we do not add an extra one from the paste content.
9138      */
prepareSpacesAroundPaste(int min, int max, CharSequence paste)9139     private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
9140         if (paste.length() > 0) {
9141             if (min > 0) {
9142                 final char charBefore = mTransformed.charAt(min - 1);
9143                 final char charAfter = paste.charAt(0);
9144 
9145                 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9146                     // Two spaces at beginning of paste: remove one
9147                     final int originalLength = mText.length();
9148                     ((Editable) mText).delete(min - 1, min);
9149                     // Due to filters, there is no guarantee that exactly one character was
9150                     // removed: count instead.
9151                     final int delta = mText.length() - originalLength;
9152                     min += delta;
9153                     max += delta;
9154                 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9155                         !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9156                     // No space at beginning of paste: add one
9157                     final int originalLength = mText.length();
9158                     ((Editable) mText).replace(min, min, " ");
9159                     // Taking possible filters into account as above.
9160                     final int delta = mText.length() - originalLength;
9161                     min += delta;
9162                     max += delta;
9163                 }
9164             }
9165 
9166             if (max < mText.length()) {
9167                 final char charBefore = paste.charAt(paste.length() - 1);
9168                 final char charAfter = mTransformed.charAt(max);
9169 
9170                 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9171                     // Two spaces at end of paste: remove one
9172                     ((Editable) mText).delete(max, max + 1);
9173                 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9174                         !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9175                     // No space at end of paste: add one
9176                     ((Editable) mText).replace(max, max, " ");
9177                 }
9178             }
9179         }
9180 
9181         return packRangeInLong(min, max);
9182     }
9183 
getTextThumbnailBuilder(CharSequence text)9184     private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
9185         TextView shadowView = (TextView) inflate(mContext,
9186                 com.android.internal.R.layout.text_drag_thumbnail, null);
9187 
9188         if (shadowView == null) {
9189             throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
9190         }
9191 
9192         if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
9193             text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
9194         }
9195         shadowView.setText(text);
9196         shadowView.setTextColor(getTextColors());
9197 
9198         shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
9199         shadowView.setGravity(Gravity.CENTER);
9200 
9201         shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
9202                 ViewGroup.LayoutParams.WRAP_CONTENT));
9203 
9204         final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
9205         shadowView.measure(size, size);
9206 
9207         shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
9208         shadowView.invalidate();
9209         return new DragShadowBuilder(shadowView);
9210     }
9211 
9212     private static class DragLocalState {
9213         public TextView sourceTextView;
9214         public int start, end;
9215 
DragLocalState(TextView sourceTextView, int start, int end)9216         public DragLocalState(TextView sourceTextView, int start, int end) {
9217             this.sourceTextView = sourceTextView;
9218             this.start = start;
9219             this.end = end;
9220         }
9221     }
9222 
9223     @Override
performLongClick()9224     public boolean performLongClick() {
9225         boolean handled = false;
9226         boolean vibrate = true;
9227 
9228         if (super.performLongClick()) {
9229             mDiscardNextActionUp = true;
9230             handled = true;
9231         }
9232 
9233         // Long press in empty space moves cursor and shows the Paste affordance if available.
9234         if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
9235                 mInsertionControllerEnabled) {
9236             final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY);
9237             stopSelectionActionMode();
9238             Selection.setSelection((Spannable) mText, offset);
9239             getInsertionController().showWithActionPopup();
9240             handled = true;
9241             vibrate = false;
9242         }
9243 
9244         if (!handled && mSelectionActionMode != null) {
9245             if (touchPositionIsInSelection()) {
9246                 // Start a drag
9247                 final int start = getSelectionStart();
9248                 final int end = getSelectionEnd();
9249                 CharSequence selectedText = getTransformedText(start, end);
9250                 ClipData data = ClipData.newPlainText(null, selectedText);
9251                 DragLocalState localState = new DragLocalState(this, start, end);
9252                 startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
9253                 stopSelectionActionMode();
9254             } else {
9255                 getSelectionController().hide();
9256                 selectCurrentWord();
9257                 getSelectionController().show();
9258             }
9259             handled = true;
9260         }
9261 
9262         // Start a new selection
9263         if (!handled) {
9264             vibrate = handled = startSelectionActionMode();
9265         }
9266 
9267         if (vibrate) {
9268             performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
9269         }
9270 
9271         if (handled) {
9272             mDiscardNextActionUp = true;
9273         }
9274 
9275         return handled;
9276     }
9277 
touchPositionIsInSelection()9278     private boolean touchPositionIsInSelection() {
9279         int selectionStart = getSelectionStart();
9280         int selectionEnd = getSelectionEnd();
9281 
9282         if (selectionStart == selectionEnd) {
9283             return false;
9284         }
9285 
9286         if (selectionStart > selectionEnd) {
9287             int tmp = selectionStart;
9288             selectionStart = selectionEnd;
9289             selectionEnd = tmp;
9290             Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
9291         }
9292 
9293         SelectionModifierCursorController selectionController = getSelectionController();
9294         int minOffset = selectionController.getMinTouchOffset();
9295         int maxOffset = selectionController.getMaxTouchOffset();
9296 
9297         return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
9298     }
9299 
getPositionListener()9300     private PositionListener getPositionListener() {
9301         if (mPositionListener == null) {
9302             mPositionListener = new PositionListener();
9303         }
9304         return mPositionListener;
9305     }
9306 
9307     private interface TextViewPositionListener {
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)9308         public void updatePosition(int parentPositionX, int parentPositionY,
9309                 boolean parentPositionChanged, boolean parentScrolled);
9310     }
9311 
9312     private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
9313         // 3 handles
9314         // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
9315         private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
9316         private TextViewPositionListener[] mPositionListeners =
9317                 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
9318         private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
9319         private boolean mPositionHasChanged = true;
9320         // Absolute position of the TextView with respect to its parent window
9321         private int mPositionX, mPositionY;
9322         private int mNumberOfListeners;
9323         private boolean mScrollHasChanged;
9324 
addSubscriber(TextViewPositionListener positionListener, boolean canMove)9325         public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
9326             if (mNumberOfListeners == 0) {
9327                 updatePosition();
9328                 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9329                 vto.addOnPreDrawListener(this);
9330             }
9331 
9332             int emptySlotIndex = -1;
9333             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9334                 TextViewPositionListener listener = mPositionListeners[i];
9335                 if (listener == positionListener) {
9336                     return;
9337                 } else if (emptySlotIndex < 0 && listener == null) {
9338                     emptySlotIndex = i;
9339                 }
9340             }
9341 
9342             mPositionListeners[emptySlotIndex] = positionListener;
9343             mCanMove[emptySlotIndex] = canMove;
9344             mNumberOfListeners++;
9345         }
9346 
removeSubscriber(TextViewPositionListener positionListener)9347         public void removeSubscriber(TextViewPositionListener positionListener) {
9348             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9349                 if (mPositionListeners[i] == positionListener) {
9350                     mPositionListeners[i] = null;
9351                     mNumberOfListeners--;
9352                     break;
9353                 }
9354             }
9355 
9356             if (mNumberOfListeners == 0) {
9357                 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9358                 vto.removeOnPreDrawListener(this);
9359             }
9360         }
9361 
getPositionX()9362         public int getPositionX() {
9363             return mPositionX;
9364         }
9365 
getPositionY()9366         public int getPositionY() {
9367             return mPositionY;
9368         }
9369 
9370         @Override
onPreDraw()9371         public boolean onPreDraw() {
9372             updatePosition();
9373 
9374             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9375                 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
9376                     TextViewPositionListener positionListener = mPositionListeners[i];
9377                     if (positionListener != null) {
9378                         positionListener.updatePosition(mPositionX, mPositionY,
9379                                 mPositionHasChanged, mScrollHasChanged);
9380                     }
9381                 }
9382             }
9383 
9384             mScrollHasChanged = false;
9385             return true;
9386         }
9387 
updatePosition()9388         private void updatePosition() {
9389             TextView.this.getLocationInWindow(mTempCoords);
9390 
9391             mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
9392 
9393             mPositionX = mTempCoords[0];
9394             mPositionY = mTempCoords[1];
9395         }
9396 
isVisible(int positionX, int positionY)9397         public boolean isVisible(int positionX, int positionY) {
9398             final TextView textView = TextView.this;
9399 
9400             if (mTempRect == null) mTempRect = new Rect();
9401             final Rect clip = mTempRect;
9402             clip.left = getCompoundPaddingLeft();
9403             clip.top = getExtendedPaddingTop();
9404             clip.right = textView.getWidth() - getCompoundPaddingRight();
9405             clip.bottom = textView.getHeight() - getExtendedPaddingBottom();
9406 
9407             final ViewParent parent = textView.getParent();
9408             if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
9409                 return false;
9410             }
9411 
9412             int posX = mPositionX + positionX;
9413             int posY = mPositionY + positionY;
9414 
9415             // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
9416             return posX >= clip.left - 1 && posX <= clip.right + 1 &&
9417                     posY >= clip.top && posY <= clip.bottom;
9418         }
9419 
isOffsetVisible(int offset)9420         public boolean isOffsetVisible(int offset) {
9421             final int line = mLayout.getLineForOffset(offset);
9422             final int lineBottom = mLayout.getLineBottom(line);
9423             final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
9424             return isVisible(primaryHorizontal + viewportToContentHorizontalOffset(),
9425                     lineBottom + viewportToContentVerticalOffset());
9426         }
9427 
onScrollChanged()9428         public void onScrollChanged() {
9429             mScrollHasChanged = true;
9430         }
9431     }
9432 
9433     @Override
onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert)9434     protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
9435         super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
9436         if (mPositionListener != null) {
9437             mPositionListener.onScrollChanged();
9438         }
9439     }
9440 
9441     private abstract class PinnedPopupWindow implements TextViewPositionListener {
9442         protected PopupWindow mPopupWindow;
9443         protected ViewGroup mContentView;
9444         int mPositionX, mPositionY;
9445 
createPopupWindow()9446         protected abstract void createPopupWindow();
initContentView()9447         protected abstract void initContentView();
getTextOffset()9448         protected abstract int getTextOffset();
getVerticalLocalPosition(int line)9449         protected abstract int getVerticalLocalPosition(int line);
clipVertically(int positionY)9450         protected abstract int clipVertically(int positionY);
9451 
PinnedPopupWindow()9452         public PinnedPopupWindow() {
9453             createPopupWindow();
9454 
9455             mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
9456             mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
9457             mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
9458 
9459             initContentView();
9460 
9461             LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
9462                     ViewGroup.LayoutParams.WRAP_CONTENT);
9463             mContentView.setLayoutParams(wrapContent);
9464 
9465             mPopupWindow.setContentView(mContentView);
9466         }
9467 
show()9468         public void show() {
9469             TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */);
9470 
9471             computeLocalPosition();
9472 
9473             final PositionListener positionListener = TextView.this.getPositionListener();
9474             updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
9475         }
9476 
measureContent()9477         protected void measureContent() {
9478             final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9479             mContentView.measure(
9480                     View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
9481                             View.MeasureSpec.AT_MOST),
9482                     View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
9483                             View.MeasureSpec.AT_MOST));
9484         }
9485 
9486         /* The popup window will be horizontally centered on the getTextOffset() and vertically
9487          * positioned according to viewportToContentHorizontalOffset.
9488          *
9489          * This method assumes that mContentView has properly been measured from its content. */
computeLocalPosition()9490         private void computeLocalPosition() {
9491             measureContent();
9492             final int width = mContentView.getMeasuredWidth();
9493             final int offset = getTextOffset();
9494             mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f);
9495             mPositionX += viewportToContentHorizontalOffset();
9496 
9497             final int line = mLayout.getLineForOffset(offset);
9498             mPositionY = getVerticalLocalPosition(line);
9499             mPositionY += viewportToContentVerticalOffset();
9500         }
9501 
updatePosition(int parentPositionX, int parentPositionY)9502         private void updatePosition(int parentPositionX, int parentPositionY) {
9503             int positionX = parentPositionX + mPositionX;
9504             int positionY = parentPositionY + mPositionY;
9505 
9506             positionY = clipVertically(positionY);
9507 
9508             // Horizontal clipping
9509             final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9510             final int width = mContentView.getMeasuredWidth();
9511             positionX = Math.min(displayMetrics.widthPixels - width, positionX);
9512             positionX = Math.max(0, positionX);
9513 
9514             if (isShowing()) {
9515                 mPopupWindow.update(positionX, positionY, -1, -1);
9516             } else {
9517                 mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
9518                         positionX, positionY);
9519             }
9520         }
9521 
hide()9522         public void hide() {
9523             mPopupWindow.dismiss();
9524             TextView.this.getPositionListener().removeSubscriber(this);
9525         }
9526 
9527         @Override
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)9528         public void updatePosition(int parentPositionX, int parentPositionY,
9529                 boolean parentPositionChanged, boolean parentScrolled) {
9530             // Either parentPositionChanged or parentScrolled is true, check if still visible
9531             if (isShowing() && getPositionListener().isOffsetVisible(getTextOffset())) {
9532                 if (parentScrolled) computeLocalPosition();
9533                 updatePosition(parentPositionX, parentPositionY);
9534             } else {
9535                 hide();
9536             }
9537         }
9538 
isShowing()9539         public boolean isShowing() {
9540             return mPopupWindow.isShowing();
9541         }
9542     }
9543 
9544     private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
9545         private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
9546         private static final int ADD_TO_DICTIONARY = -1;
9547         private static final int DELETE_TEXT = -2;
9548         private SuggestionInfo[] mSuggestionInfos;
9549         private int mNumberOfSuggestions;
9550         private boolean mCursorWasVisibleBeforeSuggestions;
9551         private SuggestionAdapter mSuggestionsAdapter;
9552         private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
9553         private final HashMap<SuggestionSpan, Integer> mSpansLengths;
9554 
9555         private class CustomPopupWindow extends PopupWindow {
CustomPopupWindow(Context context, int defStyle)9556             public CustomPopupWindow(Context context, int defStyle) {
9557                 super(context, null, defStyle);
9558             }
9559 
9560             @Override
dismiss()9561             public void dismiss() {
9562                 super.dismiss();
9563 
9564                 TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
9565 
9566                 // Safe cast since show() checks that mText is an Editable
9567                 ((Spannable) mText).removeSpan(mSuggestionRangeSpan);
9568 
9569                 setCursorVisible(mCursorWasVisibleBeforeSuggestions);
9570                 if (hasInsertionController()) {
9571                     getInsertionController().show();
9572                 }
9573             }
9574         }
9575 
SuggestionsPopupWindow()9576         public SuggestionsPopupWindow() {
9577             mCursorWasVisibleBeforeSuggestions = mCursorVisible;
9578             mSuggestionSpanComparator = new SuggestionSpanComparator();
9579             mSpansLengths = new HashMap<SuggestionSpan, Integer>();
9580         }
9581 
9582         @Override
createPopupWindow()9583         protected void createPopupWindow() {
9584             mPopupWindow = new CustomPopupWindow(TextView.this.mContext,
9585                 com.android.internal.R.attr.textSuggestionsWindowStyle);
9586             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
9587             mPopupWindow.setFocusable(true);
9588             mPopupWindow.setClippingEnabled(false);
9589         }
9590 
9591         @Override
initContentView()9592         protected void initContentView() {
9593             ListView listView = new ListView(TextView.this.getContext());
9594             mSuggestionsAdapter = new SuggestionAdapter();
9595             listView.setAdapter(mSuggestionsAdapter);
9596             listView.setOnItemClickListener(this);
9597             mContentView = listView;
9598 
9599             // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
9600             mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
9601             for (int i = 0; i < mSuggestionInfos.length; i++) {
9602                 mSuggestionInfos[i] = new SuggestionInfo();
9603             }
9604         }
9605 
9606         private class SuggestionInfo {
9607             int suggestionStart, suggestionEnd; // range of actual suggestion within text
9608             SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
9609             int suggestionIndex; // the index of this suggestion inside suggestionSpan
9610             SpannableStringBuilder text = new SpannableStringBuilder();
9611             TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
9612                     android.R.style.TextAppearance_SuggestionHighlight);
9613 
removeMisspelledFlag()9614             void removeMisspelledFlag() {
9615                 int suggestionSpanFlags = suggestionSpan.getFlags();
9616                 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
9617                     suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
9618                     suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
9619                     suggestionSpan.setFlags(suggestionSpanFlags);
9620                 }
9621             }
9622         }
9623 
9624         private class SuggestionAdapter extends BaseAdapter {
9625             private LayoutInflater mInflater = (LayoutInflater) TextView.this.mContext.
9626                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
9627 
9628             @Override
getCount()9629             public int getCount() {
9630                 return mNumberOfSuggestions;
9631             }
9632 
9633             @Override
getItem(int position)9634             public Object getItem(int position) {
9635                 return mSuggestionInfos[position];
9636             }
9637 
9638             @Override
getItemId(int position)9639             public long getItemId(int position) {
9640                 return position;
9641             }
9642 
9643             @Override
getView(int position, View convertView, ViewGroup parent)9644             public View getView(int position, View convertView, ViewGroup parent) {
9645                 TextView textView = (TextView) convertView;
9646 
9647                 if (textView == null) {
9648                     textView = (TextView) mInflater.inflate(mTextEditSuggestionItemLayout, parent,
9649                             false);
9650                 }
9651 
9652                 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
9653                 textView.setText(suggestionInfo.text);
9654 
9655                 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
9656                     textView.setCompoundDrawablesWithIntrinsicBounds(
9657                             com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
9658                 } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
9659                     textView.setCompoundDrawablesWithIntrinsicBounds(
9660                             com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
9661                 } else {
9662                     textView.setCompoundDrawables(null, null, null, null);
9663                 }
9664 
9665                 return textView;
9666             }
9667         }
9668 
9669         private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
compare(SuggestionSpan span1, SuggestionSpan span2)9670             public int compare(SuggestionSpan span1, SuggestionSpan span2) {
9671                 final int flag1 = span1.getFlags();
9672                 final int flag2 = span2.getFlags();
9673                 if (flag1 != flag2) {
9674                     // The order here should match what is used in updateDrawState
9675                     final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9676                     final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9677                     final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9678                     final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9679                     if (easy1 && !misspelled1) return -1;
9680                     if (easy2 && !misspelled2) return 1;
9681                     if (misspelled1) return -1;
9682                     if (misspelled2) return 1;
9683                 }
9684 
9685                 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
9686             }
9687         }
9688 
9689         /**
9690          * Returns the suggestion spans that cover the current cursor position. The suggestion
9691          * spans are sorted according to the length of text that they are attached to.
9692          */
getSuggestionSpans()9693         private SuggestionSpan[] getSuggestionSpans() {
9694             int pos = TextView.this.getSelectionStart();
9695             Spannable spannable = (Spannable) TextView.this.mText;
9696             SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
9697 
9698             mSpansLengths.clear();
9699             for (SuggestionSpan suggestionSpan : suggestionSpans) {
9700                 int start = spannable.getSpanStart(suggestionSpan);
9701                 int end = spannable.getSpanEnd(suggestionSpan);
9702                 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
9703             }
9704 
9705             // The suggestions are sorted according to their types (easy correction first, then
9706             // misspelled) and to the length of the text that they cover (shorter first).
9707             Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
9708             return suggestionSpans;
9709         }
9710 
9711         @Override
show()9712         public void show() {
9713             if (!(mText instanceof Editable)) return;
9714 
9715             updateSuggestions();
9716             mCursorWasVisibleBeforeSuggestions = mCursorVisible;
9717             setCursorVisible(false);
9718             super.show();
9719         }
9720 
9721         @Override
measureContent()9722         protected void measureContent() {
9723             final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9724             final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
9725                     displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
9726             final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
9727                     displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
9728 
9729             int width = 0;
9730             View view = null;
9731             for (int i = 0; i < mNumberOfSuggestions; i++) {
9732                 view = mSuggestionsAdapter.getView(i, view, mContentView);
9733                 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
9734                 view.measure(horizontalMeasure, verticalMeasure);
9735                 width = Math.max(width, view.getMeasuredWidth());
9736             }
9737 
9738             // Enforce the width based on actual text widths
9739             mContentView.measure(
9740                     View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
9741                     verticalMeasure);
9742 
9743             Drawable popupBackground = mPopupWindow.getBackground();
9744             if (popupBackground != null) {
9745                 if (mTempRect == null) mTempRect = new Rect();
9746                 popupBackground.getPadding(mTempRect);
9747                 width += mTempRect.left + mTempRect.right;
9748             }
9749             mPopupWindow.setWidth(width);
9750         }
9751 
9752         @Override
getTextOffset()9753         protected int getTextOffset() {
9754             return getSelectionStart();
9755         }
9756 
9757         @Override
getVerticalLocalPosition(int line)9758         protected int getVerticalLocalPosition(int line) {
9759             return mLayout.getLineBottom(line);
9760         }
9761 
9762         @Override
clipVertically(int positionY)9763         protected int clipVertically(int positionY) {
9764             final int height = mContentView.getMeasuredHeight();
9765             final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9766             return Math.min(positionY, displayMetrics.heightPixels - height);
9767         }
9768 
9769         @Override
hide()9770         public void hide() {
9771             super.hide();
9772         }
9773 
updateSuggestions()9774         private void updateSuggestions() {
9775             Spannable spannable = (Spannable) TextView.this.mText;
9776             SuggestionSpan[] suggestionSpans = getSuggestionSpans();
9777 
9778             final int nbSpans = suggestionSpans.length;
9779 
9780             mNumberOfSuggestions = 0;
9781             int spanUnionStart = mText.length();
9782             int spanUnionEnd = 0;
9783 
9784             SuggestionSpan misspelledSpan = null;
9785             int underlineColor = 0;
9786 
9787             for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
9788                 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
9789                 final int spanStart = spannable.getSpanStart(suggestionSpan);
9790                 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
9791                 spanUnionStart = Math.min(spanStart, spanUnionStart);
9792                 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
9793 
9794                 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
9795                     misspelledSpan = suggestionSpan;
9796                 }
9797 
9798                 // The first span dictates the background color of the highlighted text
9799                 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
9800 
9801                 String[] suggestions = suggestionSpan.getSuggestions();
9802                 int nbSuggestions = suggestions.length;
9803                 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
9804                     SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
9805                     suggestionInfo.suggestionSpan = suggestionSpan;
9806                     suggestionInfo.suggestionIndex = suggestionIndex;
9807                     suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9808                             suggestions[suggestionIndex]);
9809 
9810                     mNumberOfSuggestions++;
9811                     if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
9812                         // Also end outer for loop
9813                         spanIndex = nbSpans;
9814                         break;
9815                     }
9816                 }
9817             }
9818 
9819             for (int i = 0; i < mNumberOfSuggestions; i++) {
9820                 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
9821             }
9822 
9823             // Add to dictionary item is there a span with the misspelled flag
9824             if (misspelledSpan != null) {
9825                 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
9826                 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
9827                 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
9828                     SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
9829                     suggestionInfo.suggestionSpan = misspelledSpan;
9830                     suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
9831                     suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9832                             getContext().getString(com.android.internal.R.string.addToDictionary));
9833                     suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
9834                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9835 
9836                     mNumberOfSuggestions++;
9837                 }
9838             }
9839 
9840             // Delete item
9841             SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
9842             suggestionInfo.suggestionSpan = null;
9843             suggestionInfo.suggestionIndex = DELETE_TEXT;
9844             suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9845                     getContext().getString(com.android.internal.R.string.deleteText));
9846             suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
9847                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9848             mNumberOfSuggestions++;
9849 
9850             if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
9851             if (underlineColor == 0) {
9852                 // Fallback on the default highlight color when the first span does not provide one
9853                 mSuggestionRangeSpan.setBackgroundColor(mHighlightColor);
9854             } else {
9855                 final float BACKGROUND_TRANSPARENCY = 0.4f;
9856                 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
9857                 mSuggestionRangeSpan.setBackgroundColor(
9858                         (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
9859             }
9860             spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
9861                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9862 
9863             mSuggestionsAdapter.notifyDataSetChanged();
9864         }
9865 
highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, int unionEnd)9866         private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
9867                 int unionEnd) {
9868             final Spannable text = (Spannable) mText;
9869             final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
9870             final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
9871 
9872             // Adjust the start/end of the suggestion span
9873             suggestionInfo.suggestionStart = spanStart - unionStart;
9874             suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
9875                     + suggestionInfo.text.length();
9876 
9877             suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
9878                     suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9879 
9880             // Add the text before and after the span.
9881             suggestionInfo.text.insert(0, mText.toString().substring(unionStart, spanStart));
9882             suggestionInfo.text.append(mText.toString().substring(spanEnd, unionEnd));
9883         }
9884 
9885         @Override
onItemClick(AdapterView<?> parent, View view, int position, long id)9886         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
9887             TextView textView = (TextView) view;
9888             Editable editable = (Editable) mText;
9889 
9890             SuggestionInfo suggestionInfo = mSuggestionInfos[position];
9891 
9892             if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
9893                 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
9894                 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
9895                 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
9896                     // Do not leave two adjacent spaces after deletion, or one at beginning of text
9897                     if (spanUnionEnd < editable.length() &&
9898                             Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
9899                             (spanUnionStart == 0 ||
9900                             Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
9901                         spanUnionEnd = spanUnionEnd + 1;
9902                     }
9903                     editable.replace(spanUnionStart, spanUnionEnd, "");
9904                 }
9905                 hide();
9906                 return;
9907             }
9908 
9909             final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
9910             final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
9911             if (spanStart < 0 || spanEnd < 0) {
9912                 // Span has been removed
9913                 hide();
9914                 return;
9915             }
9916             final String originalText = mText.toString().substring(spanStart, spanEnd);
9917 
9918             if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
9919                 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
9920                 intent.putExtra("word", originalText);
9921                 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
9922                 getContext().startActivity(intent);
9923                 // There is no way to know if the word was indeed added. Re-check.
9924                 editable.removeSpan(suggestionInfo.suggestionSpan);
9925                 updateSpellCheckSpans(spanStart, spanEnd);
9926             } else {
9927                 // SuggestionSpans are removed by replace: save them before
9928                 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
9929                         SuggestionSpan.class);
9930                 final int length = suggestionSpans.length;
9931                 int[] suggestionSpansStarts = new int[length];
9932                 int[] suggestionSpansEnds = new int[length];
9933                 int[] suggestionSpansFlags = new int[length];
9934                 for (int i = 0; i < length; i++) {
9935                     final SuggestionSpan suggestionSpan = suggestionSpans[i];
9936                     suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
9937                     suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
9938                     suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
9939                 }
9940 
9941                 final int suggestionStart = suggestionInfo.suggestionStart;
9942                 final int suggestionEnd = suggestionInfo.suggestionEnd;
9943                 final String suggestion = textView.getText().subSequence(
9944                         suggestionStart, suggestionEnd).toString();
9945                 editable.replace(spanStart, spanEnd, suggestion);
9946 
9947                 suggestionInfo.removeMisspelledFlag();
9948 
9949                 // Notify source IME of the suggestion pick. Do this before swaping texts.
9950                 if (!TextUtils.isEmpty(
9951                         suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
9952                     InputMethodManager imm = InputMethodManager.peekInstance();
9953                     if (imm != null) {
9954                         imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
9955                                 suggestionInfo.suggestionIndex);
9956                     }
9957                 }
9958 
9959                 // Swap text content between actual text and Suggestion span
9960                 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
9961                 suggestions[suggestionInfo.suggestionIndex] = originalText;
9962 
9963                 // Restore previous SuggestionSpans
9964                 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
9965                 for (int i = 0; i < length; i++) {
9966                     // Only spans that include the modified region make sense after replacement
9967                     // Spans partially included in the replaced region are removed, there is no
9968                     // way to assign them a valid range after replacement
9969                     if (suggestionSpansStarts[i] <= spanStart &&
9970                             suggestionSpansEnds[i] >= spanEnd) {
9971                         editable.setSpan(suggestionSpans[i], suggestionSpansStarts[i],
9972                                 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
9973                     }
9974                 }
9975 
9976                 // Move cursor at the end of the replaced word
9977                 Selection.setSelection(editable, spanEnd + lengthDifference);
9978             }
9979 
9980             hide();
9981         }
9982     }
9983 
9984     /**
9985      * Removes the suggestion spans.
9986      */
removeSuggestionSpans(CharSequence text)9987     CharSequence removeSuggestionSpans(CharSequence text) {
9988        if (text instanceof Spanned) {
9989            Spannable spannable;
9990            if (text instanceof Spannable) {
9991                spannable = (Spannable) text;
9992            } else {
9993                spannable = new SpannableString(text);
9994                text = spannable;
9995            }
9996 
9997            SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
9998            for (int i = 0; i < spans.length; i++) {
9999                spannable.removeSpan(spans[i]);
10000            }
10001        }
10002        return text;
10003     }
10004 
showSuggestions()10005     void showSuggestions() {
10006         if (mSuggestionsPopupWindow == null) {
10007             mSuggestionsPopupWindow = new SuggestionsPopupWindow();
10008         }
10009         hideControllers();
10010         mSuggestionsPopupWindow.show();
10011     }
10012 
areSuggestionsShown()10013     boolean areSuggestionsShown() {
10014         return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
10015     }
10016 
10017     /**
10018      * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
10019      * by the IME or by the spell checker as the user types. This is done by adding
10020      * {@link SuggestionSpan}s to the text.
10021      *
10022      * When suggestions are enabled (default), this list of suggestions will be displayed when the
10023      * user asks for them on these parts of the text. This value depends on the inputType of this
10024      * TextView.
10025      *
10026      * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}.
10027      *
10028      * In addition, the type variation must be one of
10029      * {@link InputType#TYPE_TEXT_VARIATION_NORMAL},
10030      * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
10031      * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
10032      * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
10033      * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
10034      *
10035      * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set.
10036      *
10037      * @return true if the suggestions popup window is enabled, based on the inputType.
10038      */
isSuggestionsEnabled()10039     public boolean isSuggestionsEnabled() {
10040         if ((mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) return false;
10041         if ((mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false;
10042 
10043         final int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
10044         return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL ||
10045                 variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT ||
10046                 variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE ||
10047                 variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE ||
10048                 variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
10049     }
10050 
10051     /**
10052      * If provided, this ActionMode.Callback will be used to create the ActionMode when text
10053      * selection is initiated in this View.
10054      *
10055      * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
10056      * Paste actions, depending on what this View supports.
10057      *
10058      * A custom implementation can add new entries in the default menu in its
10059      * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The
10060      * default actions can also be removed from the menu using {@link Menu#removeItem(int)} and
10061      * passing {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy}
10062      * or {@link android.R.id#paste} ids as parameters.
10063      *
10064      * Returning false from
10065      * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will prevent
10066      * the action mode from being started.
10067      *
10068      * Action click events should be handled by the custom implementation of
10069      * {@link android.view.ActionMode.Callback#onActionItemClicked(ActionMode, MenuItem)}.
10070      *
10071      * Note that text selection mode is not started when a TextView receives focus and the
10072      * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
10073      * that case, to allow for quick replacement.
10074      */
setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback)10075     public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
10076         mCustomSelectionActionModeCallback = actionModeCallback;
10077     }
10078 
10079     /**
10080      * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
10081      *
10082      * @return The current custom selection callback.
10083      */
getCustomSelectionActionModeCallback()10084     public ActionMode.Callback getCustomSelectionActionModeCallback() {
10085         return mCustomSelectionActionModeCallback;
10086     }
10087 
10088     /**
10089      *
10090      * @return true if the selection mode was actually started.
10091      */
startSelectionActionMode()10092     private boolean startSelectionActionMode() {
10093         if (mSelectionActionMode != null) {
10094             // Selection action mode is already started
10095             return false;
10096         }
10097 
10098         if (!canSelectText() || !requestFocus()) {
10099             Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
10100             return false;
10101         }
10102 
10103         if (!hasSelection()) {
10104             // There may already be a selection on device rotation
10105             if (!selectCurrentWord()) {
10106                 // No word found under cursor or text selection not permitted.
10107                 return false;
10108             }
10109         }
10110 
10111         final InputMethodManager imm = InputMethodManager.peekInstance();
10112         boolean extractedTextModeWillBeStartedFullScreen = !(this instanceof ExtractEditText) &&
10113                 imm != null && imm.isFullscreenMode();
10114 
10115         // Do not start the action mode when extracted text will show up full screen, thus
10116         // immediately hiding the newly created action bar, which would be visually distracting.
10117         if (!extractedTextModeWillBeStartedFullScreen) {
10118             ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
10119             mSelectionActionMode = startActionMode(actionModeCallback);
10120         }
10121         final boolean selectionStarted = mSelectionActionMode != null ||
10122                 extractedTextModeWillBeStartedFullScreen;
10123 
10124         if (selectionStarted && !mTextIsSelectable && imm != null) {
10125             // Show the IME to be able to replace text, except when selecting non editable text.
10126             imm.showSoftInput(this, 0, null);
10127         }
10128 
10129         return selectionStarted;
10130     }
10131 
stopSelectionActionMode()10132     private void stopSelectionActionMode() {
10133         if (mSelectionActionMode != null) {
10134             // This will hide the mSelectionModifierCursorController
10135             mSelectionActionMode.finish();
10136         }
10137     }
10138 
10139     /**
10140      * Paste clipboard content between min and max positions.
10141      */
paste(int min, int max)10142     private void paste(int min, int max) {
10143         ClipboardManager clipboard =
10144             (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
10145         ClipData clip = clipboard.getPrimaryClip();
10146         if (clip != null) {
10147             boolean didFirst = false;
10148             for (int i=0; i<clip.getItemCount(); i++) {
10149                 CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
10150                 if (paste != null) {
10151                     if (!didFirst) {
10152                         long minMax = prepareSpacesAroundPaste(min, max, paste);
10153                         min = extractRangeStartFromLong(minMax);
10154                         max = extractRangeEndFromLong(minMax);
10155                         Selection.setSelection((Spannable) mText, max);
10156                         ((Editable) mText).replace(min, max, paste);
10157                         didFirst = true;
10158                     } else {
10159                         ((Editable) mText).insert(getSelectionEnd(), "\n");
10160                         ((Editable) mText).insert(getSelectionEnd(), paste);
10161                     }
10162                 }
10163             }
10164             stopSelectionActionMode();
10165             sLastCutOrCopyTime = 0;
10166         }
10167     }
10168 
setPrimaryClip(ClipData clip)10169     private void setPrimaryClip(ClipData clip) {
10170         ClipboardManager clipboard = (ClipboardManager) getContext().
10171                 getSystemService(Context.CLIPBOARD_SERVICE);
10172         clipboard.setPrimaryClip(clip);
10173         sLastCutOrCopyTime = SystemClock.uptimeMillis();
10174     }
10175 
10176     /**
10177      * An ActionMode Callback class that is used to provide actions while in text selection mode.
10178      *
10179      * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
10180      * on which of these this TextView supports.
10181      */
10182     private class SelectionActionModeCallback implements ActionMode.Callback {
10183 
10184         @Override
onCreateActionMode(ActionMode mode, Menu menu)10185         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
10186             TypedArray styledAttributes = mContext.obtainStyledAttributes(
10187                     com.android.internal.R.styleable.SelectionModeDrawables);
10188 
10189             boolean allowText = getContext().getResources().getBoolean(
10190                     com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
10191 
10192             mode.setTitle(allowText ?
10193                     mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
10194             mode.setSubtitle(null);
10195 
10196             int selectAllIconId = 0; // No icon by default
10197             if (!allowText) {
10198                 // Provide an icon, text will not be displayed on smaller screens.
10199                 selectAllIconId = styledAttributes.getResourceId(
10200                         R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
10201             }
10202 
10203             menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
10204                     setIcon(selectAllIconId).
10205                     setAlphabeticShortcut('a').
10206                     setShowAsAction(
10207                             MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
10208 
10209             if (canCut()) {
10210                 menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
10211                     setIcon(styledAttributes.getResourceId(
10212                             R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
10213                     setAlphabeticShortcut('x').
10214                     setShowAsAction(
10215                             MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
10216             }
10217 
10218             if (canCopy()) {
10219                 menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
10220                     setIcon(styledAttributes.getResourceId(
10221                             R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
10222                     setAlphabeticShortcut('c').
10223                     setShowAsAction(
10224                             MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
10225             }
10226 
10227             if (canPaste()) {
10228                 menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
10229                         setIcon(styledAttributes.getResourceId(
10230                                 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
10231                         setAlphabeticShortcut('v').
10232                         setShowAsAction(
10233                                 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
10234             }
10235 
10236             styledAttributes.recycle();
10237 
10238             if (mCustomSelectionActionModeCallback != null) {
10239                 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
10240                     // The custom mode can choose to cancel the action mode
10241                     return false;
10242                 }
10243             }
10244 
10245             if (menu.hasVisibleItems() || mode.getCustomView() != null) {
10246                 getSelectionController().show();
10247                 return true;
10248             } else {
10249                 return false;
10250             }
10251         }
10252 
10253         @Override
onPrepareActionMode(ActionMode mode, Menu menu)10254         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
10255             if (mCustomSelectionActionModeCallback != null) {
10256                 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
10257             }
10258             return true;
10259         }
10260 
10261         @Override
onActionItemClicked(ActionMode mode, MenuItem item)10262         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
10263             if (mCustomSelectionActionModeCallback != null &&
10264                  mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
10265                 return true;
10266             }
10267             return onTextContextMenuItem(item.getItemId());
10268         }
10269 
10270         @Override
onDestroyActionMode(ActionMode mode)10271         public void onDestroyActionMode(ActionMode mode) {
10272             if (mCustomSelectionActionModeCallback != null) {
10273                 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
10274             }
10275             Selection.setSelection((Spannable) mText, getSelectionEnd());
10276 
10277             if (mSelectionModifierCursorController != null) {
10278                 mSelectionModifierCursorController.hide();
10279             }
10280 
10281             mSelectionActionMode = null;
10282         }
10283     }
10284 
10285     private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
10286         private static final int POPUP_TEXT_LAYOUT =
10287                 com.android.internal.R.layout.text_edit_action_popup_text;
10288         private TextView mPasteTextView;
10289         private TextView mReplaceTextView;
10290 
10291         @Override
createPopupWindow()10292         protected void createPopupWindow() {
10293             mPopupWindow = new PopupWindow(TextView.this.mContext, null,
10294                     com.android.internal.R.attr.textSelectHandleWindowStyle);
10295             mPopupWindow.setClippingEnabled(true);
10296         }
10297 
10298         @Override
initContentView()10299         protected void initContentView() {
10300             LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
10301             linearLayout.setOrientation(LinearLayout.HORIZONTAL);
10302             mContentView = linearLayout;
10303             mContentView.setBackgroundResource(
10304                     com.android.internal.R.drawable.text_edit_paste_window);
10305 
10306             LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
10307                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
10308 
10309             LayoutParams wrapContent = new LayoutParams(
10310                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
10311 
10312             mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
10313             mPasteTextView.setLayoutParams(wrapContent);
10314             mContentView.addView(mPasteTextView);
10315             mPasteTextView.setText(com.android.internal.R.string.paste);
10316             mPasteTextView.setOnClickListener(this);
10317 
10318             mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
10319             mReplaceTextView.setLayoutParams(wrapContent);
10320             mContentView.addView(mReplaceTextView);
10321             mReplaceTextView.setText(com.android.internal.R.string.replace);
10322             mReplaceTextView.setOnClickListener(this);
10323         }
10324 
10325         @Override
show()10326         public void show() {
10327             boolean canPaste = canPaste();
10328             boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
10329             mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
10330             mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
10331 
10332             if (!canPaste && !canSuggest) return;
10333 
10334             super.show();
10335         }
10336 
10337         @Override
onClick(View view)10338         public void onClick(View view) {
10339             if (view == mPasteTextView && canPaste()) {
10340                 onTextContextMenuItem(ID_PASTE);
10341                 hide();
10342             } else if (view == mReplaceTextView) {
10343                 final int middle = (getSelectionStart() + getSelectionEnd()) / 2;
10344                 stopSelectionActionMode();
10345                 Selection.setSelection((Spannable) mText, middle);
10346                 showSuggestions();
10347             }
10348         }
10349 
10350         @Override
getTextOffset()10351         protected int getTextOffset() {
10352             return (getSelectionStart() + getSelectionEnd()) / 2;
10353         }
10354 
10355         @Override
getVerticalLocalPosition(int line)10356         protected int getVerticalLocalPosition(int line) {
10357             return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
10358         }
10359 
10360         @Override
clipVertically(int positionY)10361         protected int clipVertically(int positionY) {
10362             if (positionY < 0) {
10363                 final int offset = getTextOffset();
10364                 final int line = mLayout.getLineForOffset(offset);
10365                 positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line);
10366                 positionY += mContentView.getMeasuredHeight();
10367 
10368                 // Assumes insertion and selection handles share the same height
10369                 final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
10370                 positionY += handle.getIntrinsicHeight();
10371             }
10372 
10373             return positionY;
10374         }
10375     }
10376 
10377     private abstract class HandleView extends View implements TextViewPositionListener {
10378         protected Drawable mDrawable;
10379         protected Drawable mDrawableLtr;
10380         protected Drawable mDrawableRtl;
10381         private final PopupWindow mContainer;
10382         // Position with respect to the parent TextView
10383         private int mPositionX, mPositionY;
10384         private boolean mIsDragging;
10385         // Offset from touch position to mPosition
10386         private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
10387         protected int mHotspotX;
10388         // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
10389         private float mTouchOffsetY;
10390         // Where the touch position should be on the handle to ensure a maximum cursor visibility
10391         private float mIdealVerticalOffset;
10392         // Parent's (TextView) previous position in window
10393         private int mLastParentX, mLastParentY;
10394         // Transient action popup window for Paste and Replace actions
10395         protected ActionPopupWindow mActionPopupWindow;
10396         // Previous text character offset
10397         private int mPreviousOffset = -1;
10398         // Previous text character offset
10399         private boolean mPositionHasChanged = true;
10400         // Used to delay the appearance of the action popup window
10401         private Runnable mActionPopupShower;
10402 
HandleView(Drawable drawableLtr, Drawable drawableRtl)10403         public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
10404             super(TextView.this.mContext);
10405             mContainer = new PopupWindow(TextView.this.mContext, null,
10406                     com.android.internal.R.attr.textSelectHandleWindowStyle);
10407             mContainer.setSplitTouchEnabled(true);
10408             mContainer.setClippingEnabled(false);
10409             mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
10410             mContainer.setContentView(this);
10411 
10412             mDrawableLtr = drawableLtr;
10413             mDrawableRtl = drawableRtl;
10414 
10415             updateDrawable();
10416 
10417             final int handleHeight = mDrawable.getIntrinsicHeight();
10418             mTouchOffsetY = -0.3f * handleHeight;
10419             mIdealVerticalOffset = 0.7f * handleHeight;
10420         }
10421 
updateDrawable()10422         protected void updateDrawable() {
10423             final int offset = getCurrentCursorOffset();
10424             final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset);
10425             mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
10426             mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
10427         }
10428 
getHotspotX(Drawable drawable, boolean isRtlRun)10429         protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
10430 
10431         // Touch-up filter: number of previous positions remembered
10432         private static final int HISTORY_SIZE = 5;
10433         private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
10434         private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
10435         private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
10436         private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
10437         private int mPreviousOffsetIndex = 0;
10438         private int mNumberPreviousOffsets = 0;
10439 
startTouchUpFilter(int offset)10440         private void startTouchUpFilter(int offset) {
10441             mNumberPreviousOffsets = 0;
10442             addPositionToTouchUpFilter(offset);
10443         }
10444 
addPositionToTouchUpFilter(int offset)10445         private void addPositionToTouchUpFilter(int offset) {
10446             mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
10447             mPreviousOffsets[mPreviousOffsetIndex] = offset;
10448             mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
10449             mNumberPreviousOffsets++;
10450         }
10451 
filterOnTouchUp()10452         private void filterOnTouchUp() {
10453             final long now = SystemClock.uptimeMillis();
10454             int i = 0;
10455             int index = mPreviousOffsetIndex;
10456             final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
10457             while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
10458                 i++;
10459                 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
10460             }
10461 
10462             if (i > 0 && i < iMax &&
10463                     (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
10464                 positionAtCursorOffset(mPreviousOffsets[index], false);
10465             }
10466         }
10467 
offsetHasBeenChanged()10468         public boolean offsetHasBeenChanged() {
10469             return mNumberPreviousOffsets > 1;
10470         }
10471 
10472         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)10473         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
10474             setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
10475         }
10476 
show()10477         public void show() {
10478             if (isShowing()) return;
10479 
10480             getPositionListener().addSubscriber(this, true /* local position may change */);
10481 
10482             // Make sure the offset is always considered new, even when focusing at same position
10483             mPreviousOffset = -1;
10484             positionAtCursorOffset(getCurrentCursorOffset(), false);
10485 
10486             hideActionPopupWindow();
10487         }
10488 
dismiss()10489         protected void dismiss() {
10490             mIsDragging = false;
10491             mContainer.dismiss();
10492             onDetached();
10493         }
10494 
hide()10495         public void hide() {
10496             dismiss();
10497 
10498             TextView.this.getPositionListener().removeSubscriber(this);
10499         }
10500 
showActionPopupWindow(int delay)10501         void showActionPopupWindow(int delay) {
10502             if (mActionPopupWindow == null) {
10503                 mActionPopupWindow = new ActionPopupWindow();
10504             }
10505             if (mActionPopupShower == null) {
10506                 mActionPopupShower = new Runnable() {
10507                     public void run() {
10508                         mActionPopupWindow.show();
10509                     }
10510                 };
10511             } else {
10512                 TextView.this.removeCallbacks(mActionPopupShower);
10513             }
10514             TextView.this.postDelayed(mActionPopupShower, delay);
10515         }
10516 
hideActionPopupWindow()10517         protected void hideActionPopupWindow() {
10518             if (mActionPopupShower != null) {
10519                 TextView.this.removeCallbacks(mActionPopupShower);
10520             }
10521             if (mActionPopupWindow != null) {
10522                 mActionPopupWindow.hide();
10523             }
10524         }
10525 
isShowing()10526         public boolean isShowing() {
10527             return mContainer.isShowing();
10528         }
10529 
isVisible()10530         private boolean isVisible() {
10531             // Always show a dragging handle.
10532             if (mIsDragging) {
10533                 return true;
10534             }
10535 
10536             if (isInBatchEditMode()) {
10537                 return false;
10538             }
10539 
10540             return getPositionListener().isVisible(mPositionX + mHotspotX, mPositionY);
10541         }
10542 
getCurrentCursorOffset()10543         public abstract int getCurrentCursorOffset();
10544 
updateSelection(int offset)10545         protected abstract void updateSelection(int offset);
10546 
updatePosition(float x, float y)10547         public abstract void updatePosition(float x, float y);
10548 
positionAtCursorOffset(int offset, boolean parentScrolled)10549         protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
10550             // A HandleView relies on the layout, which may be nulled by external methods
10551             if (mLayout == null) {
10552                 // Will update controllers' state, hiding them and stopping selection mode if needed
10553                 prepareCursorControllers();
10554                 return;
10555             }
10556 
10557             if (offset != mPreviousOffset || parentScrolled) {
10558                 updateSelection(offset);
10559                 addPositionToTouchUpFilter(offset);
10560                 final int line = mLayout.getLineForOffset(offset);
10561 
10562                 mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
10563                 mPositionY = mLayout.getLineBottom(line);
10564 
10565                 // Take TextView's padding and scroll into account.
10566                 mPositionX += viewportToContentHorizontalOffset();
10567                 mPositionY += viewportToContentVerticalOffset();
10568 
10569                 mPreviousOffset = offset;
10570                 mPositionHasChanged = true;
10571             }
10572         }
10573 
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)10574         public void updatePosition(int parentPositionX, int parentPositionY,
10575                 boolean parentPositionChanged, boolean parentScrolled) {
10576             positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
10577             if (parentPositionChanged || mPositionHasChanged) {
10578                 if (mIsDragging) {
10579                     // Update touchToWindow offset in case of parent scrolling while dragging
10580                     if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
10581                         mTouchToWindowOffsetX += parentPositionX - mLastParentX;
10582                         mTouchToWindowOffsetY += parentPositionY - mLastParentY;
10583                         mLastParentX = parentPositionX;
10584                         mLastParentY = parentPositionY;
10585                     }
10586 
10587                     onHandleMoved();
10588                 }
10589 
10590                 if (isVisible()) {
10591                     final int positionX = parentPositionX + mPositionX;
10592                     final int positionY = parentPositionY + mPositionY;
10593                     if (isShowing()) {
10594                         mContainer.update(positionX, positionY, -1, -1);
10595                     } else {
10596                         mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
10597                                 positionX, positionY);
10598                     }
10599                 } else {
10600                     if (isShowing()) {
10601                         dismiss();
10602                     }
10603                 }
10604 
10605                 mPositionHasChanged = false;
10606             }
10607         }
10608 
10609         @Override
onDraw(Canvas c)10610         protected void onDraw(Canvas c) {
10611             mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
10612             mDrawable.draw(c);
10613         }
10614 
10615         @Override
onTouchEvent(MotionEvent ev)10616         public boolean onTouchEvent(MotionEvent ev) {
10617             switch (ev.getActionMasked()) {
10618                 case MotionEvent.ACTION_DOWN: {
10619                     startTouchUpFilter(getCurrentCursorOffset());
10620                     mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
10621                     mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
10622 
10623                     final PositionListener positionListener = getPositionListener();
10624                     mLastParentX = positionListener.getPositionX();
10625                     mLastParentY = positionListener.getPositionY();
10626                     mIsDragging = true;
10627                     break;
10628                 }
10629 
10630                 case MotionEvent.ACTION_MOVE: {
10631                     final float rawX = ev.getRawX();
10632                     final float rawY = ev.getRawY();
10633 
10634                     // Vertical hysteresis: vertical down movement tends to snap to ideal offset
10635                     final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
10636                     final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
10637                     float newVerticalOffset;
10638                     if (previousVerticalOffset < mIdealVerticalOffset) {
10639                         newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
10640                         newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
10641                     } else {
10642                         newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
10643                         newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
10644                     }
10645                     mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
10646 
10647                     final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
10648                     final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
10649 
10650                     updatePosition(newPosX, newPosY);
10651                     break;
10652                 }
10653 
10654                 case MotionEvent.ACTION_UP:
10655                     filterOnTouchUp();
10656                     mIsDragging = false;
10657                     break;
10658 
10659                 case MotionEvent.ACTION_CANCEL:
10660                     mIsDragging = false;
10661                     break;
10662             }
10663             return true;
10664         }
10665 
isDragging()10666         public boolean isDragging() {
10667             return mIsDragging;
10668         }
10669 
onHandleMoved()10670         void onHandleMoved() {
10671             hideActionPopupWindow();
10672         }
10673 
onDetached()10674         public void onDetached() {
10675             hideActionPopupWindow();
10676         }
10677     }
10678 
10679     private class InsertionHandleView extends HandleView {
10680         private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
10681         private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
10682 
10683         // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
10684         private float mDownPositionX, mDownPositionY;
10685         private Runnable mHider;
10686 
InsertionHandleView(Drawable drawable)10687         public InsertionHandleView(Drawable drawable) {
10688             super(drawable, drawable);
10689         }
10690 
10691         @Override
show()10692         public void show() {
10693             super.show();
10694 
10695             final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime;
10696             if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
10697                 showActionPopupWindow(0);
10698             }
10699 
10700             hideAfterDelay();
10701         }
10702 
showWithActionPopup()10703         public void showWithActionPopup() {
10704             show();
10705             showActionPopupWindow(0);
10706         }
10707 
hideAfterDelay()10708         private void hideAfterDelay() {
10709             removeHiderCallback();
10710             if (mHider == null) {
10711                 mHider = new Runnable() {
10712                     public void run() {
10713                         hide();
10714                     }
10715                 };
10716             }
10717             TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
10718         }
10719 
removeHiderCallback()10720         private void removeHiderCallback() {
10721             if (mHider != null) {
10722                 TextView.this.removeCallbacks(mHider);
10723             }
10724         }
10725 
10726         @Override
getHotspotX(Drawable drawable, boolean isRtlRun)10727         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10728             return drawable.getIntrinsicWidth() / 2;
10729         }
10730 
10731         @Override
onTouchEvent(MotionEvent ev)10732         public boolean onTouchEvent(MotionEvent ev) {
10733             final boolean result = super.onTouchEvent(ev);
10734 
10735             switch (ev.getActionMasked()) {
10736                 case MotionEvent.ACTION_DOWN:
10737                     mDownPositionX = ev.getRawX();
10738                     mDownPositionY = ev.getRawY();
10739                     break;
10740 
10741                 case MotionEvent.ACTION_UP:
10742                     if (!offsetHasBeenChanged()) {
10743                         final float deltaX = mDownPositionX - ev.getRawX();
10744                         final float deltaY = mDownPositionY - ev.getRawY();
10745                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
10746                         if (distanceSquared < mSquaredTouchSlopDistance) {
10747                             if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
10748                                 // Tapping on the handle dismisses the displayed action popup
10749                                 mActionPopupWindow.hide();
10750                             } else {
10751                                 showWithActionPopup();
10752                             }
10753                         }
10754                     }
10755                     hideAfterDelay();
10756                     break;
10757 
10758                 case MotionEvent.ACTION_CANCEL:
10759                     hideAfterDelay();
10760                     break;
10761 
10762                 default:
10763                     break;
10764             }
10765 
10766             return result;
10767         }
10768 
10769         @Override
getCurrentCursorOffset()10770         public int getCurrentCursorOffset() {
10771             return TextView.this.getSelectionStart();
10772         }
10773 
10774         @Override
updateSelection(int offset)10775         public void updateSelection(int offset) {
10776             Selection.setSelection((Spannable) mText, offset);
10777         }
10778 
10779         @Override
updatePosition(float x, float y)10780         public void updatePosition(float x, float y) {
10781             positionAtCursorOffset(getOffsetForPosition(x, y), false);
10782         }
10783 
10784         @Override
onHandleMoved()10785         void onHandleMoved() {
10786             super.onHandleMoved();
10787             removeHiderCallback();
10788         }
10789 
10790         @Override
onDetached()10791         public void onDetached() {
10792             super.onDetached();
10793             removeHiderCallback();
10794         }
10795     }
10796 
10797     private class SelectionStartHandleView extends HandleView {
10798 
SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl)10799         public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10800             super(drawableLtr, drawableRtl);
10801         }
10802 
10803         @Override
getHotspotX(Drawable drawable, boolean isRtlRun)10804         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10805             if (isRtlRun) {
10806                 return drawable.getIntrinsicWidth() / 4;
10807             } else {
10808                 return (drawable.getIntrinsicWidth() * 3) / 4;
10809             }
10810         }
10811 
10812         @Override
getCurrentCursorOffset()10813         public int getCurrentCursorOffset() {
10814             return TextView.this.getSelectionStart();
10815         }
10816 
10817         @Override
updateSelection(int offset)10818         public void updateSelection(int offset) {
10819             Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
10820             updateDrawable();
10821         }
10822 
10823         @Override
updatePosition(float x, float y)10824         public void updatePosition(float x, float y) {
10825             int offset = getOffsetForPosition(x, y);
10826 
10827             // Handles can not cross and selection is at least one character
10828             final int selectionEnd = getSelectionEnd();
10829             if (offset >= selectionEnd) offset = selectionEnd - 1;
10830 
10831             positionAtCursorOffset(offset, false);
10832         }
10833 
getActionPopupWindow()10834         public ActionPopupWindow getActionPopupWindow() {
10835             return mActionPopupWindow;
10836         }
10837     }
10838 
10839     private class SelectionEndHandleView extends HandleView {
10840 
SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl)10841         public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10842             super(drawableLtr, drawableRtl);
10843         }
10844 
10845         @Override
getHotspotX(Drawable drawable, boolean isRtlRun)10846         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10847             if (isRtlRun) {
10848                 return (drawable.getIntrinsicWidth() * 3) / 4;
10849             } else {
10850                 return drawable.getIntrinsicWidth() / 4;
10851             }
10852         }
10853 
10854         @Override
getCurrentCursorOffset()10855         public int getCurrentCursorOffset() {
10856             return TextView.this.getSelectionEnd();
10857         }
10858 
10859         @Override
updateSelection(int offset)10860         public void updateSelection(int offset) {
10861             Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
10862             updateDrawable();
10863         }
10864 
10865         @Override
updatePosition(float x, float y)10866         public void updatePosition(float x, float y) {
10867             int offset = getOffsetForPosition(x, y);
10868 
10869             // Handles can not cross and selection is at least one character
10870             final int selectionStart = getSelectionStart();
10871             if (offset <= selectionStart) offset = selectionStart + 1;
10872 
10873             positionAtCursorOffset(offset, false);
10874         }
10875 
setActionPopupWindow(ActionPopupWindow actionPopupWindow)10876         public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
10877             mActionPopupWindow = actionPopupWindow;
10878         }
10879     }
10880 
10881     /**
10882      * A CursorController instance can be used to control a cursor in the text.
10883      * It is not used outside of {@link TextView}.
10884      * @hide
10885      */
10886     private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
10887         /**
10888          * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
10889          * See also {@link #hide()}.
10890          */
show()10891         public void show();
10892 
10893         /**
10894          * Hide the cursor controller from screen.
10895          * See also {@link #show()}.
10896          */
hide()10897         public void hide();
10898 
10899         /**
10900          * Called when the view is detached from window. Perform house keeping task, such as
10901          * stopping Runnable thread that would otherwise keep a reference on the context, thus
10902          * preventing the activity from being recycled.
10903          */
onDetached()10904         public void onDetached();
10905     }
10906 
10907     private class InsertionPointCursorController implements CursorController {
10908         private InsertionHandleView mHandle;
10909 
show()10910         public void show() {
10911             getHandle().show();
10912         }
10913 
showWithActionPopup()10914         public void showWithActionPopup() {
10915             getHandle().showWithActionPopup();
10916         }
10917 
hide()10918         public void hide() {
10919             if (mHandle != null) {
10920                 mHandle.hide();
10921             }
10922         }
10923 
onTouchModeChanged(boolean isInTouchMode)10924         public void onTouchModeChanged(boolean isInTouchMode) {
10925             if (!isInTouchMode) {
10926                 hide();
10927             }
10928         }
10929 
getHandle()10930         private InsertionHandleView getHandle() {
10931             if (mSelectHandleCenter == null) {
10932                 mSelectHandleCenter = mContext.getResources().getDrawable(
10933                         mTextSelectHandleRes);
10934             }
10935             if (mHandle == null) {
10936                 mHandle = new InsertionHandleView(mSelectHandleCenter);
10937             }
10938             return mHandle;
10939         }
10940 
10941         @Override
onDetached()10942         public void onDetached() {
10943             final ViewTreeObserver observer = getViewTreeObserver();
10944             observer.removeOnTouchModeChangeListener(this);
10945 
10946             if (mHandle != null) mHandle.onDetached();
10947         }
10948     }
10949 
10950     private class SelectionModifierCursorController implements CursorController {
10951         private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
10952         // The cursor controller handles, lazily created when shown.
10953         private SelectionStartHandleView mStartHandle;
10954         private SelectionEndHandleView mEndHandle;
10955         // The offsets of that last touch down event. Remembered to start selection there.
10956         private int mMinTouchOffset, mMaxTouchOffset;
10957 
10958         // Double tap detection
10959         private long mPreviousTapUpTime = 0;
10960         private float mPreviousTapPositionX, mPreviousTapPositionY;
10961 
SelectionModifierCursorController()10962         SelectionModifierCursorController() {
10963             resetTouchOffsets();
10964         }
10965 
show()10966         public void show() {
10967             if (isInBatchEditMode()) {
10968                 return;
10969             }
10970             initDrawables();
10971             initHandles();
10972             hideInsertionPointCursorController();
10973         }
10974 
initDrawables()10975         private void initDrawables() {
10976             if (mSelectHandleLeft == null) {
10977                 mSelectHandleLeft = mContext.getResources().getDrawable(
10978                         mTextSelectHandleLeftRes);
10979             }
10980             if (mSelectHandleRight == null) {
10981                 mSelectHandleRight = mContext.getResources().getDrawable(
10982                         mTextSelectHandleRightRes);
10983             }
10984         }
10985 
initHandles()10986         private void initHandles() {
10987             // Lazy object creation has to be done before updatePosition() is called.
10988             if (mStartHandle == null) {
10989                 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
10990             }
10991             if (mEndHandle == null) {
10992                 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
10993             }
10994 
10995             mStartHandle.show();
10996             mEndHandle.show();
10997 
10998             // Make sure both left and right handles share the same ActionPopupWindow (so that
10999             // moving any of the handles hides the action popup).
11000             mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
11001             mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
11002 
11003             hideInsertionPointCursorController();
11004         }
11005 
hide()11006         public void hide() {
11007             if (mStartHandle != null) mStartHandle.hide();
11008             if (mEndHandle != null) mEndHandle.hide();
11009         }
11010 
onTouchEvent(MotionEvent event)11011         public void onTouchEvent(MotionEvent event) {
11012             // This is done even when the View does not have focus, so that long presses can start
11013             // selection and tap can move cursor from this tap position.
11014             switch (event.getActionMasked()) {
11015                 case MotionEvent.ACTION_DOWN:
11016                     final float x = event.getX();
11017                     final float y = event.getY();
11018 
11019                     // Remember finger down position, to be able to start selection from there
11020                     mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
11021 
11022                     // Double tap detection
11023                     long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
11024                     if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
11025                             isPositionOnText(x, y)) {
11026                         final float deltaX = x - mPreviousTapPositionX;
11027                         final float deltaY = y - mPreviousTapPositionY;
11028                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
11029                         if (distanceSquared < mSquaredTouchSlopDistance) {
11030                             startSelectionActionMode();
11031                             mDiscardNextActionUp = true;
11032                         }
11033                     }
11034 
11035                     mPreviousTapPositionX = x;
11036                     mPreviousTapPositionY = y;
11037                     break;
11038 
11039                 case MotionEvent.ACTION_POINTER_DOWN:
11040                 case MotionEvent.ACTION_POINTER_UP:
11041                     // Handle multi-point gestures. Keep min and max offset positions.
11042                     // Only activated for devices that correctly handle multi-touch.
11043                     if (mContext.getPackageManager().hasSystemFeature(
11044                             PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
11045                         updateMinAndMaxOffsets(event);
11046                     }
11047                     break;
11048 
11049                 case MotionEvent.ACTION_UP:
11050                     mPreviousTapUpTime = SystemClock.uptimeMillis();
11051                     break;
11052             }
11053         }
11054 
11055         /**
11056          * @param event
11057          */
updateMinAndMaxOffsets(MotionEvent event)11058         private void updateMinAndMaxOffsets(MotionEvent event) {
11059             int pointerCount = event.getPointerCount();
11060             for (int index = 0; index < pointerCount; index++) {
11061                 int offset = getOffsetForPosition(event.getX(index), event.getY(index));
11062                 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
11063                 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
11064             }
11065         }
11066 
getMinTouchOffset()11067         public int getMinTouchOffset() {
11068             return mMinTouchOffset;
11069         }
11070 
getMaxTouchOffset()11071         public int getMaxTouchOffset() {
11072             return mMaxTouchOffset;
11073         }
11074 
resetTouchOffsets()11075         public void resetTouchOffsets() {
11076             mMinTouchOffset = mMaxTouchOffset = -1;
11077         }
11078 
11079         /**
11080          * @return true iff this controller is currently used to move the selection start.
11081          */
isSelectionStartDragged()11082         public boolean isSelectionStartDragged() {
11083             return mStartHandle != null && mStartHandle.isDragging();
11084         }
11085 
onTouchModeChanged(boolean isInTouchMode)11086         public void onTouchModeChanged(boolean isInTouchMode) {
11087             if (!isInTouchMode) {
11088                 hide();
11089             }
11090         }
11091 
11092         @Override
onDetached()11093         public void onDetached() {
11094             final ViewTreeObserver observer = getViewTreeObserver();
11095             observer.removeOnTouchModeChangeListener(this);
11096 
11097             if (mStartHandle != null) mStartHandle.onDetached();
11098             if (mEndHandle != null) mEndHandle.onDetached();
11099         }
11100     }
11101 
hideInsertionPointCursorController()11102     private void hideInsertionPointCursorController() {
11103         // No need to create the controller to hide it.
11104         if (mInsertionPointCursorController != null) {
11105             mInsertionPointCursorController.hide();
11106         }
11107     }
11108 
11109     /**
11110      * Hides the insertion controller and stops text selection mode, hiding the selection controller
11111      */
hideControllers()11112     private void hideControllers() {
11113         hideCursorControllers();
11114         hideSpanControllers();
11115     }
11116 
hideSpanControllers()11117     private void hideSpanControllers() {
11118         if (mChangeWatcher != null) {
11119             mChangeWatcher.hideControllers();
11120         }
11121     }
11122 
hideCursorControllers()11123     private void hideCursorControllers() {
11124         hideInsertionPointCursorController();
11125         stopSelectionActionMode();
11126     }
11127 
11128     /**
11129      * Get the character offset closest to the specified absolute position. A typical use case is to
11130      * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
11131      *
11132      * @param x The horizontal absolute position of a point on screen
11133      * @param y The vertical absolute position of a point on screen
11134      * @return the character offset for the character whose position is closest to the specified
11135      *  position. Returns -1 if there is no layout.
11136      */
getOffsetForPosition(float x, float y)11137     public int getOffsetForPosition(float x, float y) {
11138         if (getLayout() == null) return -1;
11139         final int line = getLineAtCoordinate(y);
11140         final int offset = getOffsetAtCoordinate(line, x);
11141         return offset;
11142     }
11143 
convertToLocalHorizontalCoordinate(float x)11144     private float convertToLocalHorizontalCoordinate(float x) {
11145         x -= getTotalPaddingLeft();
11146         // Clamp the position to inside of the view.
11147         x = Math.max(0.0f, x);
11148         x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
11149         x += getScrollX();
11150         return x;
11151     }
11152 
getLineAtCoordinate(float y)11153     private int getLineAtCoordinate(float y) {
11154         y -= getTotalPaddingTop();
11155         // Clamp the position to inside of the view.
11156         y = Math.max(0.0f, y);
11157         y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
11158         y += getScrollY();
11159         return getLayout().getLineForVertical((int) y);
11160     }
11161 
getOffsetAtCoordinate(int line, float x)11162     private int getOffsetAtCoordinate(int line, float x) {
11163         x = convertToLocalHorizontalCoordinate(x);
11164         return getLayout().getOffsetForHorizontal(line, x);
11165     }
11166 
11167     /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
11168      * in the view. Returns false when the position is in the empty space of left/right of text.
11169      */
isPositionOnText(float x, float y)11170     private boolean isPositionOnText(float x, float y) {
11171         if (getLayout() == null) return false;
11172 
11173         final int line = getLineAtCoordinate(y);
11174         x = convertToLocalHorizontalCoordinate(x);
11175 
11176         if (x < getLayout().getLineLeft(line)) return false;
11177         if (x > getLayout().getLineRight(line)) return false;
11178         return true;
11179     }
11180 
11181     @Override
onDragEvent(DragEvent event)11182     public boolean onDragEvent(DragEvent event) {
11183         switch (event.getAction()) {
11184             case DragEvent.ACTION_DRAG_STARTED:
11185                 return hasInsertionController();
11186 
11187             case DragEvent.ACTION_DRAG_ENTERED:
11188                 TextView.this.requestFocus();
11189                 return true;
11190 
11191             case DragEvent.ACTION_DRAG_LOCATION:
11192                 final int offset = getOffsetForPosition(event.getX(), event.getY());
11193                 Selection.setSelection((Spannable)mText, offset);
11194                 return true;
11195 
11196             case DragEvent.ACTION_DROP:
11197                 onDrop(event);
11198                 return true;
11199 
11200             case DragEvent.ACTION_DRAG_ENDED:
11201             case DragEvent.ACTION_DRAG_EXITED:
11202             default:
11203                 return true;
11204         }
11205     }
11206 
onDrop(DragEvent event)11207     private void onDrop(DragEvent event) {
11208         StringBuilder content = new StringBuilder("");
11209         ClipData clipData = event.getClipData();
11210         final int itemCount = clipData.getItemCount();
11211         for (int i=0; i < itemCount; i++) {
11212             Item item = clipData.getItemAt(i);
11213             content.append(item.coerceToText(TextView.this.mContext));
11214         }
11215 
11216         final int offset = getOffsetForPosition(event.getX(), event.getY());
11217 
11218         Object localState = event.getLocalState();
11219         DragLocalState dragLocalState = null;
11220         if (localState instanceof DragLocalState) {
11221             dragLocalState = (DragLocalState) localState;
11222         }
11223         boolean dragDropIntoItself = dragLocalState != null &&
11224                 dragLocalState.sourceTextView == this;
11225 
11226         if (dragDropIntoItself) {
11227             if (offset >= dragLocalState.start && offset < dragLocalState.end) {
11228                 // A drop inside the original selection discards the drop.
11229                 return;
11230             }
11231         }
11232 
11233         final int originalLength = mText.length();
11234         long minMax = prepareSpacesAroundPaste(offset, offset, content);
11235         int min = extractRangeStartFromLong(minMax);
11236         int max = extractRangeEndFromLong(minMax);
11237 
11238         Selection.setSelection((Spannable) mText, max);
11239         ((Editable) mText).replace(min, max, content);
11240 
11241         if (dragDropIntoItself) {
11242             int dragSourceStart = dragLocalState.start;
11243             int dragSourceEnd = dragLocalState.end;
11244             if (max <= dragSourceStart) {
11245                 // Inserting text before selection has shifted positions
11246                 final int shift = mText.length() - originalLength;
11247                 dragSourceStart += shift;
11248                 dragSourceEnd += shift;
11249             }
11250 
11251             // Delete original selection
11252             ((Editable) mText).delete(dragSourceStart, dragSourceEnd);
11253 
11254             // Make sure we do not leave two adjacent spaces.
11255             if ((dragSourceStart == 0 ||
11256                     Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
11257                     (dragSourceStart == mText.length() ||
11258                     Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
11259                 final int pos = dragSourceStart == mText.length() ?
11260                         dragSourceStart - 1 : dragSourceStart;
11261                 ((Editable) mText).delete(pos, pos + 1);
11262             }
11263         }
11264     }
11265 
11266     /**
11267      * @return True if this view supports insertion handles.
11268      */
hasInsertionController()11269     boolean hasInsertionController() {
11270         return mInsertionControllerEnabled;
11271     }
11272 
11273     /**
11274      * @return True if this view supports selection handles.
11275      */
hasSelectionController()11276     boolean hasSelectionController() {
11277         return mSelectionControllerEnabled;
11278     }
11279 
getInsertionController()11280     InsertionPointCursorController getInsertionController() {
11281         if (!mInsertionControllerEnabled) {
11282             return null;
11283         }
11284 
11285         if (mInsertionPointCursorController == null) {
11286             mInsertionPointCursorController = new InsertionPointCursorController();
11287 
11288             final ViewTreeObserver observer = getViewTreeObserver();
11289             observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
11290         }
11291 
11292         return mInsertionPointCursorController;
11293     }
11294 
getSelectionController()11295     SelectionModifierCursorController getSelectionController() {
11296         if (!mSelectionControllerEnabled) {
11297             return null;
11298         }
11299 
11300         if (mSelectionModifierCursorController == null) {
11301             mSelectionModifierCursorController = new SelectionModifierCursorController();
11302 
11303             final ViewTreeObserver observer = getViewTreeObserver();
11304             observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
11305         }
11306 
11307         return mSelectionModifierCursorController;
11308     }
11309 
isInBatchEditMode()11310     boolean isInBatchEditMode() {
11311         final InputMethodState ims = mInputMethodState;
11312         if (ims != null) {
11313             return ims.mBatchEditNesting > 0;
11314         }
11315         return mInBatchEditControllers;
11316     }
11317 
11318     @Override
resolveTextDirection()11319     protected void resolveTextDirection() {
11320         if (hasPasswordTransformationMethod()) {
11321             mTextDir = TextDirectionHeuristics.LOCALE;
11322             return;
11323         }
11324 
11325         // Always need to resolve layout direction first
11326         final boolean defaultIsRtl = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL);
11327 
11328         // Then resolve text direction on the parent
11329         super.resolveTextDirection();
11330 
11331         // Now, we can select the heuristic
11332         int textDir = getResolvedTextDirection();
11333         switch (textDir) {
11334             default:
11335             case TEXT_DIRECTION_FIRST_STRONG:
11336                 mTextDir = (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
11337                         TextDirectionHeuristics.FIRSTSTRONG_LTR);
11338                 break;
11339             case TEXT_DIRECTION_ANY_RTL:
11340                 mTextDir = TextDirectionHeuristics.ANYRTL_LTR;
11341                 break;
11342             case TEXT_DIRECTION_LTR:
11343                 mTextDir = TextDirectionHeuristics.LTR;
11344                 break;
11345             case TEXT_DIRECTION_RTL:
11346                 mTextDir = TextDirectionHeuristics.RTL;
11347                 break;
11348         }
11349     }
11350 
11351     /**
11352      * Subclasses will need to override this method to implement their own way of resolving
11353      * drawables depending on the layout direction.
11354      *
11355      * A call to the super method will be required from the subclasses implementation.
11356      *
11357      */
resolveDrawables()11358     protected void resolveDrawables() {
11359         // No need to resolve twice
11360         if (mResolvedDrawables) {
11361             return;
11362         }
11363         // No drawable to resolve
11364         if (mDrawables == null) {
11365             return;
11366         }
11367         // No relative drawable to resolve
11368         if (mDrawables.mDrawableStart == null && mDrawables.mDrawableEnd == null) {
11369             mResolvedDrawables = true;
11370             return;
11371         }
11372 
11373         Drawables dr = mDrawables;
11374         switch(getResolvedLayoutDirection()) {
11375             case LAYOUT_DIRECTION_RTL:
11376                 if (dr.mDrawableStart != null) {
11377                     dr.mDrawableRight = dr.mDrawableStart;
11378 
11379                     dr.mDrawableSizeRight = dr.mDrawableSizeStart;
11380                     dr.mDrawableHeightRight = dr.mDrawableHeightStart;
11381                 }
11382                 if (dr.mDrawableEnd != null) {
11383                     dr.mDrawableLeft = dr.mDrawableEnd;
11384 
11385                     dr.mDrawableSizeLeft = dr.mDrawableSizeEnd;
11386                     dr.mDrawableHeightLeft = dr.mDrawableHeightEnd;
11387                 }
11388                 break;
11389 
11390             case LAYOUT_DIRECTION_LTR:
11391             default:
11392                 if (dr.mDrawableStart != null) {
11393                     dr.mDrawableLeft = dr.mDrawableStart;
11394 
11395                     dr.mDrawableSizeLeft = dr.mDrawableSizeStart;
11396                     dr.mDrawableHeightLeft = dr.mDrawableHeightStart;
11397                 }
11398                 if (dr.mDrawableEnd != null) {
11399                     dr.mDrawableRight = dr.mDrawableEnd;
11400 
11401                     dr.mDrawableSizeRight = dr.mDrawableSizeEnd;
11402                     dr.mDrawableHeightRight = dr.mDrawableHeightEnd;
11403                 }
11404                 break;
11405         }
11406         mResolvedDrawables = true;
11407     }
11408 
resetResolvedDrawables()11409     protected void resetResolvedDrawables() {
11410         mResolvedDrawables = false;
11411     }
11412 
11413     /**
11414      * @hide
11415      */
viewClicked(InputMethodManager imm)11416     protected void viewClicked(InputMethodManager imm) {
11417         if (imm != null) {
11418             imm.viewClicked(this);
11419         }
11420     }
11421 
11422     @ViewDebug.ExportedProperty(category = "text")
11423     private CharSequence            mText;
11424     private CharSequence            mTransformed;
11425     private BufferType              mBufferType = BufferType.NORMAL;
11426 
11427     private int                     mInputType = EditorInfo.TYPE_NULL;
11428     private CharSequence            mHint;
11429     private Layout                  mHintLayout;
11430 
11431     private KeyListener             mInput;
11432 
11433     private MovementMethod          mMovement;
11434     private TransformationMethod    mTransformation;
11435     private boolean                 mAllowTransformationLengthChange;
11436     private ChangeWatcher           mChangeWatcher;
11437 
11438     private ArrayList<TextWatcher>  mListeners = null;
11439 
11440     // display attributes
11441     private final TextPaint         mTextPaint;
11442     private boolean                 mUserSetTextScaleX;
11443     private final Paint             mHighlightPaint;
11444     private int                     mHighlightColor = 0x6633B5E5;
11445     /**
11446      * This is temporarily visible to fix bug 3085564 in webView. Do not rely on
11447      * this field being protected. Will be restored as private when lineHeight
11448      * feature request 3215097 is implemented
11449      * @hide
11450      */
11451     protected Layout                mLayout;
11452 
11453     private long                    mShowCursor;
11454     private Blink                   mBlink;
11455     private boolean                 mCursorVisible = true;
11456 
11457     // Cursor Controllers.
11458     private InsertionPointCursorController mInsertionPointCursorController;
11459     private SelectionModifierCursorController mSelectionModifierCursorController;
11460     private ActionMode              mSelectionActionMode;
11461     private boolean                 mInsertionControllerEnabled;
11462     private boolean                 mSelectionControllerEnabled;
11463     private boolean                 mInBatchEditControllers;
11464 
11465     // These are needed to desambiguate a long click. If the long click comes from ones of these, we
11466     // select from the current cursor position. Otherwise, select from long pressed position.
11467     private boolean                 mDPadCenterIsDown = false;
11468     private boolean                 mEnterKeyIsDown = false;
11469     private boolean                 mContextMenuTriggeredByKey = false;
11470 
11471     private boolean                 mSelectAllOnFocus = false;
11472 
11473     private int                     mGravity = Gravity.TOP | Gravity.START;
11474     private boolean                 mHorizontallyScrolling;
11475 
11476     private int                     mAutoLinkMask;
11477     private boolean                 mLinksClickable = true;
11478 
11479     private float                   mSpacingMult = 1.0f;
11480     private float                   mSpacingAdd = 0.0f;
11481     private boolean                 mTextIsSelectable = false;
11482 
11483     private static final int        LINES = 1;
11484     private static final int        EMS = LINES;
11485     private static final int        PIXELS = 2;
11486 
11487     private int                     mMaximum = Integer.MAX_VALUE;
11488     private int                     mMaxMode = LINES;
11489     private int                     mMinimum = 0;
11490     private int                     mMinMode = LINES;
11491 
11492     private int                     mOldMaximum = mMaximum;
11493     private int                     mOldMaxMode = mMaxMode;
11494 
11495     private int                     mMaxWidth = Integer.MAX_VALUE;
11496     private int                     mMaxWidthMode = PIXELS;
11497     private int                     mMinWidth = 0;
11498     private int                     mMinWidthMode = PIXELS;
11499 
11500     private boolean                 mSingleLine;
11501     private int                     mDesiredHeightAtMeasure = -1;
11502     private boolean                 mIncludePad = true;
11503 
11504     // tmp primitives, so we don't alloc them on each draw
11505     private Path                    mHighlightPath;
11506     private boolean                 mHighlightPathBogus = true;
11507     private static final RectF      sTempRect = new RectF();
11508 
11509     // XXX should be much larger
11510     private static final int        VERY_WIDE = 1024*1024;
11511 
11512     private static final int        BLINK = 500;
11513 
11514     private static final int ANIMATED_SCROLL_GAP = 250;
11515     private long mLastScroll;
11516     private Scroller mScroller = null;
11517 
11518     private BoringLayout.Metrics mBoring;
11519     private BoringLayout.Metrics mHintBoring;
11520 
11521     private BoringLayout mSavedLayout, mSavedHintLayout;
11522 
11523     private TextDirectionHeuristic mTextDir = null;
11524 
11525     private static final InputFilter[] NO_FILTERS = new InputFilter[0];
11526     private InputFilter[] mFilters = NO_FILTERS;
11527     private static final Spanned EMPTY_SPANNED = new SpannedString("");
11528     private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
11529     // System wide time for last cut or copy action.
11530     private static long sLastCutOrCopyTime;
11531     // Used to highlight a word when it is corrected by the IME
11532     private CorrectionHighlighter mCorrectionHighlighter;
11533     // New state used to change background based on whether this TextView is multiline.
11534     private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
11535 }
11536