blob: 7b9ecca075ff778db7353ce1091fcf691504dfca [file] [log] [blame]
Justin Klaassen10d07c82017-09-15 17:58:39 -04001/*
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
17package android.widget;
18
19import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH;
20import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX;
21import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
22import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
23
24import android.R;
25import android.annotation.CheckResult;
26import android.annotation.ColorInt;
27import android.annotation.DrawableRes;
28import android.annotation.FloatRange;
29import android.annotation.IntDef;
Jeff Davidsona192cc22018-02-08 15:30:06 -080030import android.annotation.IntRange;
Justin Klaassen10d07c82017-09-15 17:58:39 -040031import android.annotation.NonNull;
32import android.annotation.Nullable;
Jeff Davidsona192cc22018-02-08 15:30:06 -080033import android.annotation.Px;
Justin Klaassen10d07c82017-09-15 17:58:39 -040034import android.annotation.Size;
35import android.annotation.StringRes;
36import android.annotation.StyleRes;
37import android.annotation.XmlRes;
38import android.app.Activity;
Justin Klaassen4d01eea2018-04-03 23:21:57 -040039import android.app.PendingIntent;
Justin Klaassen10d07c82017-09-15 17:58:39 -040040import android.app.assist.AssistStructure;
41import android.content.ClipData;
42import android.content.ClipDescription;
43import android.content.ClipboardManager;
44import android.content.Context;
45import android.content.Intent;
46import android.content.UndoManager;
47import android.content.res.ColorStateList;
48import android.content.res.CompatibilityInfo;
49import android.content.res.Configuration;
Jeff Davidsona192cc22018-02-08 15:30:06 -080050import android.content.res.ResourceId;
Justin Klaassen10d07c82017-09-15 17:58:39 -040051import android.content.res.Resources;
52import android.content.res.TypedArray;
53import android.content.res.XmlResourceParser;
54import android.graphics.BaseCanvas;
55import android.graphics.Canvas;
56import android.graphics.Insets;
57import android.graphics.Paint;
Jeff Davidsona192cc22018-02-08 15:30:06 -080058import android.graphics.Paint.FontMetricsInt;
Justin Klaassen10d07c82017-09-15 17:58:39 -040059import android.graphics.Path;
60import android.graphics.PorterDuff;
61import android.graphics.Rect;
62import android.graphics.RectF;
63import android.graphics.Typeface;
64import android.graphics.drawable.Drawable;
65import android.graphics.fonts.FontVariationAxis;
66import android.icu.text.DecimalFormatSymbols;
67import android.os.AsyncTask;
68import android.os.Build.VERSION_CODES;
69import android.os.Bundle;
70import android.os.LocaleList;
71import android.os.Parcel;
72import android.os.Parcelable;
73import android.os.ParcelableParcel;
74import android.os.SystemClock;
75import android.provider.Settings;
76import android.text.BoringLayout;
77import android.text.DynamicLayout;
78import android.text.Editable;
79import android.text.GetChars;
80import android.text.GraphicsOperations;
81import android.text.InputFilter;
82import android.text.InputType;
83import android.text.Layout;
84import android.text.ParcelableSpan;
Justin Klaassen4d01eea2018-04-03 23:21:57 -040085import android.text.PrecomputedText;
Justin Klaassen10d07c82017-09-15 17:58:39 -040086import android.text.Selection;
87import android.text.SpanWatcher;
88import android.text.Spannable;
89import android.text.SpannableStringBuilder;
90import android.text.Spanned;
91import android.text.SpannedString;
92import android.text.StaticLayout;
93import android.text.TextDirectionHeuristic;
94import android.text.TextDirectionHeuristics;
95import android.text.TextPaint;
96import android.text.TextUtils;
97import android.text.TextUtils.TruncateAt;
98import android.text.TextWatcher;
99import android.text.method.AllCapsTransformationMethod;
100import android.text.method.ArrowKeyMovementMethod;
101import android.text.method.DateKeyListener;
102import android.text.method.DateTimeKeyListener;
103import android.text.method.DialerKeyListener;
104import android.text.method.DigitsKeyListener;
105import android.text.method.KeyListener;
106import android.text.method.LinkMovementMethod;
107import android.text.method.MetaKeyKeyListener;
108import android.text.method.MovementMethod;
109import android.text.method.PasswordTransformationMethod;
110import android.text.method.SingleLineTransformationMethod;
111import android.text.method.TextKeyListener;
112import android.text.method.TimeKeyListener;
113import android.text.method.TransformationMethod;
114import android.text.method.TransformationMethod2;
115import android.text.method.WordIterator;
116import android.text.style.CharacterStyle;
117import android.text.style.ClickableSpan;
118import android.text.style.ParagraphStyle;
119import android.text.style.SpellCheckSpan;
120import android.text.style.SuggestionSpan;
121import android.text.style.URLSpan;
122import android.text.style.UpdateAppearance;
123import android.text.util.Linkify;
124import android.util.AttributeSet;
125import android.util.DisplayMetrics;
126import android.util.IntArray;
127import android.util.Log;
128import android.util.SparseIntArray;
129import android.util.TypedValue;
130import android.view.AccessibilityIterators.TextSegmentIterator;
131import android.view.ActionMode;
132import android.view.Choreographer;
133import android.view.ContextMenu;
134import android.view.DragEvent;
135import android.view.Gravity;
136import android.view.HapticFeedbackConstants;
137import android.view.InputDevice;
138import android.view.KeyCharacterMap;
139import android.view.KeyEvent;
140import android.view.MotionEvent;
141import android.view.PointerIcon;
142import android.view.View;
143import android.view.ViewConfiguration;
144import android.view.ViewDebug;
145import android.view.ViewGroup.LayoutParams;
146import android.view.ViewHierarchyEncoder;
147import android.view.ViewParent;
148import android.view.ViewRootImpl;
149import android.view.ViewStructure;
150import android.view.ViewTreeObserver;
151import android.view.accessibility.AccessibilityEvent;
152import android.view.accessibility.AccessibilityManager;
153import android.view.accessibility.AccessibilityNodeInfo;
154import android.view.animation.AnimationUtils;
155import android.view.autofill.AutofillManager;
156import android.view.autofill.AutofillValue;
157import android.view.inputmethod.BaseInputConnection;
158import android.view.inputmethod.CompletionInfo;
159import android.view.inputmethod.CorrectionInfo;
160import android.view.inputmethod.CursorAnchorInfo;
161import android.view.inputmethod.EditorInfo;
162import android.view.inputmethod.ExtractedText;
163import android.view.inputmethod.ExtractedTextRequest;
164import android.view.inputmethod.InputConnection;
165import android.view.inputmethod.InputMethodManager;
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400166import android.view.textclassifier.TextClassification;
167import android.view.textclassifier.TextClassificationContext;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400168import android.view.textclassifier.TextClassificationManager;
169import android.view.textclassifier.TextClassifier;
Justin Klaassen98fe7812018-01-03 13:39:41 -0500170import android.view.textclassifier.TextLinks;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400171import android.view.textservice.SpellCheckerSubtype;
172import android.view.textservice.TextServicesManager;
173import android.widget.RemoteViews.RemoteView;
174
175import com.android.internal.annotations.VisibleForTesting;
176import com.android.internal.logging.MetricsLogger;
177import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
178import com.android.internal.util.FastMath;
Justin Klaassen98fe7812018-01-03 13:39:41 -0500179import com.android.internal.util.Preconditions;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400180import com.android.internal.widget.EditableInputConnection;
181
182import libcore.util.EmptyArray;
183
184import org.xmlpull.v1.XmlPullParserException;
185
186import java.io.IOException;
187import java.lang.annotation.Retention;
188import java.lang.annotation.RetentionPolicy;
189import java.lang.ref.WeakReference;
190import java.util.ArrayList;
191import java.util.Arrays;
192import java.util.Locale;
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400193import java.util.concurrent.CompletableFuture;
194import java.util.concurrent.TimeUnit;
195import java.util.function.Consumer;
196import java.util.function.Supplier;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400197
198/**
199 * A user interface element that displays text to the user.
200 * To provide user-editable text, see {@link EditText}.
201 * <p>
202 * The following code sample shows a typical use, with an XML layout
203 * and code to modify the contents of the text view:
204 * </p>
205
206 * <pre>
207 * &lt;LinearLayout
208 xmlns:android="http://schemas.android.com/apk/res/android"
209 android:layout_width="match_parent"
210 android:layout_height="match_parent"&gt;
211 * &lt;TextView
212 * android:id="@+id/text_view_id"
213 * android:layout_height="wrap_content"
214 * android:layout_width="wrap_content"
215 * android:text="@string/hello" /&gt;
216 * &lt;/LinearLayout&gt;
217 * </pre>
218 * <p>
219 * This code sample demonstrates how to modify the contents of the text view
220 * defined in the previous XML layout:
221 * </p>
222 * <pre>
223 * public class MainActivity extends Activity {
224 *
225 * protected void onCreate(Bundle savedInstanceState) {
226 * super.onCreate(savedInstanceState);
227 * setContentView(R.layout.activity_main);
228 * final TextView helloTextView = (TextView) findViewById(R.id.text_view_id);
229 * helloTextView.setText(R.string.user_greeting);
230 * }
231 * }
232 * </pre>
233 * <p>
234 * To customize the appearance of TextView, see <a href="https://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>.
235 * </p>
236 * <p>
237 * <b>XML attributes</b>
238 * <p>
239 * See {@link android.R.styleable#TextView TextView Attributes},
240 * {@link android.R.styleable#View View Attributes}
241 *
242 * @attr ref android.R.styleable#TextView_text
243 * @attr ref android.R.styleable#TextView_bufferType
244 * @attr ref android.R.styleable#TextView_hint
245 * @attr ref android.R.styleable#TextView_textColor
246 * @attr ref android.R.styleable#TextView_textColorHighlight
247 * @attr ref android.R.styleable#TextView_textColorHint
248 * @attr ref android.R.styleable#TextView_textAppearance
249 * @attr ref android.R.styleable#TextView_textColorLink
250 * @attr ref android.R.styleable#TextView_textSize
251 * @attr ref android.R.styleable#TextView_textScaleX
252 * @attr ref android.R.styleable#TextView_fontFamily
253 * @attr ref android.R.styleable#TextView_typeface
254 * @attr ref android.R.styleable#TextView_textStyle
255 * @attr ref android.R.styleable#TextView_cursorVisible
256 * @attr ref android.R.styleable#TextView_maxLines
257 * @attr ref android.R.styleable#TextView_maxHeight
258 * @attr ref android.R.styleable#TextView_lines
259 * @attr ref android.R.styleable#TextView_height
260 * @attr ref android.R.styleable#TextView_minLines
261 * @attr ref android.R.styleable#TextView_minHeight
262 * @attr ref android.R.styleable#TextView_maxEms
263 * @attr ref android.R.styleable#TextView_maxWidth
264 * @attr ref android.R.styleable#TextView_ems
265 * @attr ref android.R.styleable#TextView_width
266 * @attr ref android.R.styleable#TextView_minEms
267 * @attr ref android.R.styleable#TextView_minWidth
268 * @attr ref android.R.styleable#TextView_gravity
269 * @attr ref android.R.styleable#TextView_scrollHorizontally
270 * @attr ref android.R.styleable#TextView_password
271 * @attr ref android.R.styleable#TextView_singleLine
272 * @attr ref android.R.styleable#TextView_selectAllOnFocus
273 * @attr ref android.R.styleable#TextView_includeFontPadding
274 * @attr ref android.R.styleable#TextView_maxLength
275 * @attr ref android.R.styleable#TextView_shadowColor
276 * @attr ref android.R.styleable#TextView_shadowDx
277 * @attr ref android.R.styleable#TextView_shadowDy
278 * @attr ref android.R.styleable#TextView_shadowRadius
279 * @attr ref android.R.styleable#TextView_autoLink
280 * @attr ref android.R.styleable#TextView_linksClickable
281 * @attr ref android.R.styleable#TextView_numeric
282 * @attr ref android.R.styleable#TextView_digits
283 * @attr ref android.R.styleable#TextView_phoneNumber
284 * @attr ref android.R.styleable#TextView_inputMethod
285 * @attr ref android.R.styleable#TextView_capitalize
286 * @attr ref android.R.styleable#TextView_autoText
287 * @attr ref android.R.styleable#TextView_editable
288 * @attr ref android.R.styleable#TextView_freezesText
289 * @attr ref android.R.styleable#TextView_ellipsize
290 * @attr ref android.R.styleable#TextView_drawableTop
291 * @attr ref android.R.styleable#TextView_drawableBottom
292 * @attr ref android.R.styleable#TextView_drawableRight
293 * @attr ref android.R.styleable#TextView_drawableLeft
294 * @attr ref android.R.styleable#TextView_drawableStart
295 * @attr ref android.R.styleable#TextView_drawableEnd
296 * @attr ref android.R.styleable#TextView_drawablePadding
297 * @attr ref android.R.styleable#TextView_drawableTint
298 * @attr ref android.R.styleable#TextView_drawableTintMode
299 * @attr ref android.R.styleable#TextView_lineSpacingExtra
300 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400301 * @attr ref android.R.styleable#TextView_justificationMode
Justin Klaassen10d07c82017-09-15 17:58:39 -0400302 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
303 * @attr ref android.R.styleable#TextView_inputType
304 * @attr ref android.R.styleable#TextView_imeOptions
305 * @attr ref android.R.styleable#TextView_privateImeOptions
306 * @attr ref android.R.styleable#TextView_imeActionLabel
307 * @attr ref android.R.styleable#TextView_imeActionId
308 * @attr ref android.R.styleable#TextView_editorExtras
309 * @attr ref android.R.styleable#TextView_elegantTextHeight
Jeff Davidsona192cc22018-02-08 15:30:06 -0800310 * @attr ref android.R.styleable#TextView_fallbackLineSpacing
Justin Klaassen10d07c82017-09-15 17:58:39 -0400311 * @attr ref android.R.styleable#TextView_letterSpacing
312 * @attr ref android.R.styleable#TextView_fontFeatureSettings
313 * @attr ref android.R.styleable#TextView_breakStrategy
314 * @attr ref android.R.styleable#TextView_hyphenationFrequency
315 * @attr ref android.R.styleable#TextView_autoSizeTextType
316 * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
317 * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
318 * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
319 * @attr ref android.R.styleable#TextView_autoSizePresetSizes
320 */
321@RemoteView
322public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
323 static final String LOG_TAG = "TextView";
324 static final boolean DEBUG_EXTRACT = false;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400325 private static final float[] TEMP_POSITION = new float[2];
326
327 // Enum for the "typeface" XML parameter.
328 // TODO: How can we get this from the XML instead of hardcoding it here?
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400329 /** @hide */
330 @IntDef(value = {DEFAULT_TYPEFACE, SANS, SERIF, MONOSPACE})
331 @Retention(RetentionPolicy.SOURCE)
332 public @interface XMLTypefaceAttr{}
333 private static final int DEFAULT_TYPEFACE = -1;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400334 private static final int SANS = 1;
335 private static final int SERIF = 2;
336 private static final int MONOSPACE = 3;
337
338 // Enum for the "ellipsize" XML parameter.
339 private static final int ELLIPSIZE_NOT_SET = -1;
340 private static final int ELLIPSIZE_NONE = 0;
341 private static final int ELLIPSIZE_START = 1;
342 private static final int ELLIPSIZE_MIDDLE = 2;
343 private static final int ELLIPSIZE_END = 3;
344 private static final int ELLIPSIZE_MARQUEE = 4;
345
346 // Bitfield for the "numeric" XML parameter.
347 // TODO: How can we get this from the XML instead of hardcoding it here?
348 private static final int SIGNED = 2;
349 private static final int DECIMAL = 4;
350
351 /**
352 * Draw marquee text with fading edges as usual
353 */
354 private static final int MARQUEE_FADE_NORMAL = 0;
355
356 /**
357 * Draw marquee text as ellipsize end while inactive instead of with the fade.
358 * (Useful for devices where the fade can be expensive if overdone)
359 */
360 private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1;
361
362 /**
363 * Draw marquee text with fading edges because it is currently active/animating.
364 */
365 private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2;
366
367 private static final int LINES = 1;
368 private static final int EMS = LINES;
369 private static final int PIXELS = 2;
370
371 private static final RectF TEMP_RECTF = new RectF();
372
373 /** @hide */
374 static final int VERY_WIDE = 1024 * 1024; // XXX should be much larger
375 private static final int ANIMATED_SCROLL_GAP = 250;
376
377 private static final InputFilter[] NO_FILTERS = new InputFilter[0];
378 private static final Spanned EMPTY_SPANNED = new SpannedString("");
379
380 private static final int CHANGE_WATCHER_PRIORITY = 100;
381
382 // New state used to change background based on whether this TextView is multiline.
383 private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
384
385 // Accessibility action to share selected text.
386 private static final int ACCESSIBILITY_ACTION_SHARE = 0x10000000;
387
388 /**
389 * @hide
390 */
391 // Accessibility action start id for "process text" actions.
392 static final int ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID = 0x10000100;
393
394 /**
395 * @hide
396 */
397 static final int PROCESS_TEXT_REQUEST_CODE = 100;
398
399 /**
400 * Return code of {@link #doKeyDown}.
401 */
402 private static final int KEY_EVENT_NOT_HANDLED = 0;
403 private static final int KEY_EVENT_HANDLED = -1;
404 private static final int KEY_DOWN_HANDLED_BY_KEY_LISTENER = 1;
405 private static final int KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD = 2;
406
407 private static final int FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY = 500;
408
409 // System wide time for last cut, copy or text changed action.
410 static long sLastCutCopyOrTextChangedTime;
411
412 private ColorStateList mTextColor;
413 private ColorStateList mHintTextColor;
414 private ColorStateList mLinkTextColor;
415 @ViewDebug.ExportedProperty(category = "text")
416 private int mCurTextColor;
417 private int mCurHintTextColor;
418 private boolean mFreezesText;
419
420 private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
421 private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
422
423 private float mShadowRadius, mShadowDx, mShadowDy;
424 private int mShadowColor;
425
426 private boolean mPreDrawRegistered;
427 private boolean mPreDrawListenerDetached;
428
429 private TextClassifier mTextClassifier;
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400430 private TextClassifier mTextClassificationSession;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400431
432 // A flag to prevent repeated movements from escaping the enclosing text view. The idea here is
433 // that if a user is holding down a movement key to traverse text, we shouldn't also traverse
434 // the view hierarchy. On the other hand, if the user is using the movement key to traverse
435 // views (i.e. the first movement was to traverse out of this view, or this view was traversed
436 // into by the user holding the movement key down) then we shouldn't prevent the focus from
437 // changing.
438 private boolean mPreventDefaultMovement;
439
440 private TextUtils.TruncateAt mEllipsize;
441
442 static class Drawables {
443 static final int LEFT = 0;
444 static final int TOP = 1;
445 static final int RIGHT = 2;
446 static final int BOTTOM = 3;
447
448 static final int DRAWABLE_NONE = -1;
449 static final int DRAWABLE_RIGHT = 0;
450 static final int DRAWABLE_LEFT = 1;
451
452 final Rect mCompoundRect = new Rect();
453
454 final Drawable[] mShowing = new Drawable[4];
455
456 ColorStateList mTintList;
457 PorterDuff.Mode mTintMode;
458 boolean mHasTint;
459 boolean mHasTintMode;
460
461 Drawable mDrawableStart, mDrawableEnd, mDrawableError, mDrawableTemp;
462 Drawable mDrawableLeftInitial, mDrawableRightInitial;
463
464 boolean mIsRtlCompatibilityMode;
465 boolean mOverride;
466
467 int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight,
468 mDrawableSizeStart, mDrawableSizeEnd, mDrawableSizeError, mDrawableSizeTemp;
469
470 int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight,
471 mDrawableHeightStart, mDrawableHeightEnd, mDrawableHeightError, mDrawableHeightTemp;
472
473 int mDrawablePadding;
474
475 int mDrawableSaved = DRAWABLE_NONE;
476
477 public Drawables(Context context) {
478 final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
479 mIsRtlCompatibilityMode = targetSdkVersion < VERSION_CODES.JELLY_BEAN_MR1
480 || !context.getApplicationInfo().hasRtlSupport();
481 mOverride = false;
482 }
483
484 /**
485 * @return {@code true} if this object contains metadata that needs to
486 * be retained, {@code false} otherwise
487 */
488 public boolean hasMetadata() {
489 return mDrawablePadding != 0 || mHasTintMode || mHasTint;
490 }
491
492 /**
493 * Updates the list of displayed drawables to account for the current
494 * layout direction.
495 *
496 * @param layoutDirection the current layout direction
497 * @return {@code true} if the displayed drawables changed
498 */
499 public boolean resolveWithLayoutDirection(int layoutDirection) {
500 final Drawable previousLeft = mShowing[Drawables.LEFT];
501 final Drawable previousRight = mShowing[Drawables.RIGHT];
502
503 // First reset "left" and "right" drawables to their initial values
504 mShowing[Drawables.LEFT] = mDrawableLeftInitial;
505 mShowing[Drawables.RIGHT] = mDrawableRightInitial;
506
507 if (mIsRtlCompatibilityMode) {
508 // Use "start" drawable as "left" drawable if the "left" drawable was not defined
509 if (mDrawableStart != null && mShowing[Drawables.LEFT] == null) {
510 mShowing[Drawables.LEFT] = mDrawableStart;
511 mDrawableSizeLeft = mDrawableSizeStart;
512 mDrawableHeightLeft = mDrawableHeightStart;
513 }
514 // Use "end" drawable as "right" drawable if the "right" drawable was not defined
515 if (mDrawableEnd != null && mShowing[Drawables.RIGHT] == null) {
516 mShowing[Drawables.RIGHT] = mDrawableEnd;
517 mDrawableSizeRight = mDrawableSizeEnd;
518 mDrawableHeightRight = mDrawableHeightEnd;
519 }
520 } else {
521 // JB-MR1+ normal case: "start" / "end" drawables are overriding "left" / "right"
522 // drawable if and only if they have been defined
523 switch(layoutDirection) {
524 case LAYOUT_DIRECTION_RTL:
525 if (mOverride) {
526 mShowing[Drawables.RIGHT] = mDrawableStart;
527 mDrawableSizeRight = mDrawableSizeStart;
528 mDrawableHeightRight = mDrawableHeightStart;
529
530 mShowing[Drawables.LEFT] = mDrawableEnd;
531 mDrawableSizeLeft = mDrawableSizeEnd;
532 mDrawableHeightLeft = mDrawableHeightEnd;
533 }
534 break;
535
536 case LAYOUT_DIRECTION_LTR:
537 default:
538 if (mOverride) {
539 mShowing[Drawables.LEFT] = mDrawableStart;
540 mDrawableSizeLeft = mDrawableSizeStart;
541 mDrawableHeightLeft = mDrawableHeightStart;
542
543 mShowing[Drawables.RIGHT] = mDrawableEnd;
544 mDrawableSizeRight = mDrawableSizeEnd;
545 mDrawableHeightRight = mDrawableHeightEnd;
546 }
547 break;
548 }
549 }
550
551 applyErrorDrawableIfNeeded(layoutDirection);
552
553 return mShowing[Drawables.LEFT] != previousLeft
554 || mShowing[Drawables.RIGHT] != previousRight;
555 }
556
557 public void setErrorDrawable(Drawable dr, TextView tv) {
558 if (mDrawableError != dr && mDrawableError != null) {
559 mDrawableError.setCallback(null);
560 }
561 mDrawableError = dr;
562
563 if (mDrawableError != null) {
564 final Rect compoundRect = mCompoundRect;
565 final int[] state = tv.getDrawableState();
566
567 mDrawableError.setState(state);
568 mDrawableError.copyBounds(compoundRect);
569 mDrawableError.setCallback(tv);
570 mDrawableSizeError = compoundRect.width();
571 mDrawableHeightError = compoundRect.height();
572 } else {
573 mDrawableSizeError = mDrawableHeightError = 0;
574 }
575 }
576
577 private void applyErrorDrawableIfNeeded(int layoutDirection) {
578 // first restore the initial state if needed
579 switch (mDrawableSaved) {
580 case DRAWABLE_LEFT:
581 mShowing[Drawables.LEFT] = mDrawableTemp;
582 mDrawableSizeLeft = mDrawableSizeTemp;
583 mDrawableHeightLeft = mDrawableHeightTemp;
584 break;
585 case DRAWABLE_RIGHT:
586 mShowing[Drawables.RIGHT] = mDrawableTemp;
587 mDrawableSizeRight = mDrawableSizeTemp;
588 mDrawableHeightRight = mDrawableHeightTemp;
589 break;
590 case DRAWABLE_NONE:
591 default:
592 }
593 // then, if needed, assign the Error drawable to the correct location
594 if (mDrawableError != null) {
595 switch(layoutDirection) {
596 case LAYOUT_DIRECTION_RTL:
597 mDrawableSaved = DRAWABLE_LEFT;
598
599 mDrawableTemp = mShowing[Drawables.LEFT];
600 mDrawableSizeTemp = mDrawableSizeLeft;
601 mDrawableHeightTemp = mDrawableHeightLeft;
602
603 mShowing[Drawables.LEFT] = mDrawableError;
604 mDrawableSizeLeft = mDrawableSizeError;
605 mDrawableHeightLeft = mDrawableHeightError;
606 break;
607 case LAYOUT_DIRECTION_LTR:
608 default:
609 mDrawableSaved = DRAWABLE_RIGHT;
610
611 mDrawableTemp = mShowing[Drawables.RIGHT];
612 mDrawableSizeTemp = mDrawableSizeRight;
613 mDrawableHeightTemp = mDrawableHeightRight;
614
615 mShowing[Drawables.RIGHT] = mDrawableError;
616 mDrawableSizeRight = mDrawableSizeError;
617 mDrawableHeightRight = mDrawableHeightError;
618 break;
619 }
620 }
621 }
622 }
623
624 Drawables mDrawables;
625
626 private CharWrapper mCharWrapper;
627
628 private Marquee mMarquee;
629 private boolean mRestartMarquee;
630
631 private int mMarqueeRepeatLimit = 3;
632
633 private int mLastLayoutDirection = -1;
634
635 /**
636 * On some devices the fading edges add a performance penalty if used
637 * extensively in the same layout. This mode indicates how the marquee
638 * is currently being shown, if applicable. (mEllipsize will == MARQUEE)
639 */
640 private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
641
642 /**
643 * When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores
644 * the layout that should be used when the mode switches.
645 */
646 private Layout mSavedMarqueeModeLayout;
647
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400648 // Do not update following mText/mSpannable/mPrecomputed except for setTextInternal()
Justin Klaassen10d07c82017-09-15 17:58:39 -0400649 @ViewDebug.ExportedProperty(category = "text")
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400650 private @Nullable CharSequence mText;
651 private @Nullable Spannable mSpannable;
652 private @Nullable PrecomputedText mPrecomputed;
653
Justin Klaassen10d07c82017-09-15 17:58:39 -0400654 private CharSequence mTransformed;
655 private BufferType mBufferType = BufferType.NORMAL;
656
657 private CharSequence mHint;
658 private Layout mHintLayout;
659
660 private MovementMethod mMovement;
661
662 private TransformationMethod mTransformation;
663 private boolean mAllowTransformationLengthChange;
664 private ChangeWatcher mChangeWatcher;
665
666 private ArrayList<TextWatcher> mListeners;
667
668 // display attributes
669 private final TextPaint mTextPaint;
670 private boolean mUserSetTextScaleX;
671 private Layout mLayout;
672 private boolean mLocalesChanged = false;
673
674 // True if setKeyListener() has been explicitly called
675 private boolean mListenerChanged = false;
676 // True if internationalized input should be used for numbers and date and time.
677 private final boolean mUseInternationalizedInput;
678 // True if fallback fonts that end up getting used should be allowed to affect line spacing.
Jeff Davidsona192cc22018-02-08 15:30:06 -0800679 /* package */ boolean mUseFallbackLineSpacing;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400680
681 @ViewDebug.ExportedProperty(category = "text")
682 private int mGravity = Gravity.TOP | Gravity.START;
683 private boolean mHorizontallyScrolling;
684
685 private int mAutoLinkMask;
686 private boolean mLinksClickable = true;
687
688 private float mSpacingMult = 1.0f;
689 private float mSpacingAdd = 0.0f;
690
691 private int mBreakStrategy;
692 private int mHyphenationFrequency;
693 private int mJustificationMode;
694
695 private int mMaximum = Integer.MAX_VALUE;
696 private int mMaxMode = LINES;
697 private int mMinimum = 0;
698 private int mMinMode = LINES;
699
700 private int mOldMaximum = mMaximum;
701 private int mOldMaxMode = mMaxMode;
702
703 private int mMaxWidth = Integer.MAX_VALUE;
704 private int mMaxWidthMode = PIXELS;
705 private int mMinWidth = 0;
706 private int mMinWidthMode = PIXELS;
707
708 private boolean mSingleLine;
709 private int mDesiredHeightAtMeasure = -1;
710 private boolean mIncludePad = true;
711 private int mDeferScroll = -1;
712
713 // tmp primitives, so we don't alloc them on each draw
714 private Rect mTempRect;
715 private long mLastScroll;
716 private Scroller mScroller;
717 private TextPaint mTempTextPaint;
718
719 private BoringLayout.Metrics mBoring, mHintBoring;
720 private BoringLayout mSavedLayout, mSavedHintLayout;
721
722 private TextDirectionHeuristic mTextDir;
723
724 private InputFilter[] mFilters = NO_FILTERS;
725
726 private volatile Locale mCurrentSpellCheckerLocaleCache;
727
728 // It is possible to have a selection even when mEditor is null (programmatically set, like when
729 // a link is pressed). These highlight-related fields do not go in mEditor.
730 int mHighlightColor = 0x6633B5E5;
731 private Path mHighlightPath;
732 private final Paint mHighlightPaint;
733 private boolean mHighlightPathBogus = true;
734
735 // Although these fields are specific to editable text, they are not added to Editor because
736 // they are defined by the TextView's style and are theme-dependent.
737 int mCursorDrawableRes;
738 // These six fields, could be moved to Editor, since we know their default values and we
739 // could condition the creation of the Editor to a non standard value. This is however
740 // brittle since the hardcoded values here (such as
741 // com.android.internal.R.drawable.text_select_handle_left) would have to be updated if the
742 // default style is modified.
743 int mTextSelectHandleLeftRes;
744 int mTextSelectHandleRightRes;
745 int mTextSelectHandleRes;
746 int mTextEditSuggestionItemLayout;
747 int mTextEditSuggestionContainerLayout;
748 int mTextEditSuggestionHighlightStyle;
749
750 /**
751 * {@link EditText} specific data, created on demand when one of the Editor fields is used.
752 * See {@link #createEditorIfNeeded()}.
753 */
754 private Editor mEditor;
755
756 private static final int DEVICE_PROVISIONED_UNKNOWN = 0;
757 private static final int DEVICE_PROVISIONED_NO = 1;
758 private static final int DEVICE_PROVISIONED_YES = 2;
759
760 /**
761 * Some special options such as sharing selected text should only be shown if the device
762 * is provisioned. Only check the provisioned state once for a given view instance.
763 */
764 private int mDeviceProvisionedState = DEVICE_PROVISIONED_UNKNOWN;
765
766 /**
767 * The TextView does not auto-size text (default).
768 */
769 public static final int AUTO_SIZE_TEXT_TYPE_NONE = 0;
770
771 /**
772 * The TextView scales text size both horizontally and vertically to fit within the
773 * container.
774 */
775 public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1;
776
777 /** @hide */
Justin Klaassen98fe7812018-01-03 13:39:41 -0500778 @IntDef(prefix = { "AUTO_SIZE_TEXT_TYPE_" }, value = {
779 AUTO_SIZE_TEXT_TYPE_NONE,
780 AUTO_SIZE_TEXT_TYPE_UNIFORM
781 })
Justin Klaassen10d07c82017-09-15 17:58:39 -0400782 @Retention(RetentionPolicy.SOURCE)
783 public @interface AutoSizeTextType {}
784 // Default minimum size for auto-sizing text in scaled pixels.
785 private static final int DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP = 12;
786 // Default maximum size for auto-sizing text in scaled pixels.
787 private static final int DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP = 112;
788 // Default value for the step size in pixels.
789 private static final int DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX = 1;
790 // Use this to specify that any of the auto-size configuration int values have not been set.
791 private static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f;
792 // Auto-size text type.
793 private int mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
794 // Specify if auto-size text is needed.
795 private boolean mNeedsAutoSizeText = false;
796 // Step size for auto-sizing in pixels.
797 private float mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
798 // Minimum text size for auto-sizing in pixels.
799 private float mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
800 // Maximum text size for auto-sizing in pixels.
801 private float mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
802 // Contains a (specified or computed) distinct sorted set of text sizes in pixels to pick from
803 // when auto-sizing text.
804 private int[] mAutoSizeTextSizesInPx = EmptyArray.INT;
805 // Specifies whether auto-size should use the provided auto size steps set or if it should
806 // build the steps set using mAutoSizeMinTextSizeInPx, mAutoSizeMaxTextSizeInPx and
807 // mAutoSizeStepGranularityInPx.
808 private boolean mHasPresetAutoSizeValues = false;
809
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400810 // Autofill-related attributes
811 //
Jeff Davidsona192cc22018-02-08 15:30:06 -0800812 // Indicates whether the text was set statically or dynamically, so it can be used to
Justin Klaassen10d07c82017-09-15 17:58:39 -0400813 // sanitize autofill requests.
Jeff Davidsona192cc22018-02-08 15:30:06 -0800814 private boolean mTextSetFromXmlOrResourceId = false;
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400815 // Resource id used to set the text.
Jeff Davidsona192cc22018-02-08 15:30:06 -0800816 private @StringRes int mTextId = ResourceId.ID_NULL;
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400817 // Last value used on AFM.notifyValueChanged(), used to optimize autofill workflow by avoiding
818 // calls when the value did not change
819 private CharSequence mLastValueSentToAutofillManager;
820 //
821 // End of autofill-related attributes
Justin Klaassen10d07c82017-09-15 17:58:39 -0400822
823 /**
824 * Kick-start the font cache for the zygote process (to pay the cost of
825 * initializing freetype for our default font only once).
826 * @hide
827 */
828 public static void preloadFontCache() {
829 Paint p = new Paint();
830 p.setAntiAlias(true);
831 // Ensure that the Typeface is loaded here.
832 // Typically, Typeface is preloaded by zygote but not on all devices, e.g. Android Auto.
833 // So, sets Typeface.DEFAULT explicitly here for ensuring that the Typeface is loaded here
834 // since Paint.measureText can not be called without Typeface static initializer.
835 p.setTypeface(Typeface.DEFAULT);
836 // We don't care about the result, just the side-effect of measuring.
837 p.measureText("H");
838 }
839
840 /**
841 * Interface definition for a callback to be invoked when an action is
842 * performed on the editor.
843 */
844 public interface OnEditorActionListener {
845 /**
846 * Called when an action is being performed.
847 *
848 * @param v The view that was clicked.
849 * @param actionId Identifier of the action. This will be either the
850 * identifier you supplied, or {@link EditorInfo#IME_NULL
851 * EditorInfo.IME_NULL} if being called due to the enter key
852 * being pressed.
853 * @param event If triggered by an enter key, this is the event;
854 * otherwise, this is null.
855 * @return Return true if you have consumed the action, else false.
856 */
857 boolean onEditorAction(TextView v, int actionId, KeyEvent event);
858 }
859
860 public TextView(Context context) {
861 this(context, null);
862 }
863
864 public TextView(Context context, @Nullable AttributeSet attrs) {
865 this(context, attrs, com.android.internal.R.attr.textViewStyle);
866 }
867
868 public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
869 this(context, attrs, defStyleAttr, 0);
870 }
871
872 @SuppressWarnings("deprecation")
873 public TextView(
874 Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
875 super(context, attrs, defStyleAttr, defStyleRes);
876
877 // TextView is important by default, unless app developer overrode attribute.
878 if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
879 setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
880 }
881
Justin Klaassen4d01eea2018-04-03 23:21:57 -0400882 setTextInternal("");
Justin Klaassen10d07c82017-09-15 17:58:39 -0400883
884 final Resources res = getResources();
885 final CompatibilityInfo compat = res.getCompatibilityInfo();
886
887 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
888 mTextPaint.density = res.getDisplayMetrics().density;
889 mTextPaint.setCompatibilityScaling(compat.applicationScale);
890
891 mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
892 mHighlightPaint.setCompatibilityScaling(compat.applicationScale);
893
894 mMovement = getDefaultMovementMethod();
895
896 mTransformation = null;
897
898 final TextAppearanceAttributes attributes = new TextAppearanceAttributes();
899 attributes.mTextColor = ColorStateList.valueOf(0xFF000000);
900 attributes.mTextSize = 15;
901 mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
902 mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
903 mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
904
905 final Resources.Theme theme = context.getTheme();
906
907 /*
908 * Look the appearance up without checking first if it exists because
909 * almost every TextView has one and it greatly simplifies the logic
910 * to be able to parse the appearance first and then let specific tags
911 * for this View override it.
912 */
913 TypedArray a = theme.obtainStyledAttributes(attrs,
914 com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
915 TypedArray appearance = null;
916 int ap = a.getResourceId(
917 com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
918 a.recycle();
919 if (ap != -1) {
920 appearance = theme.obtainStyledAttributes(
921 ap, com.android.internal.R.styleable.TextAppearance);
922 }
923 if (appearance != null) {
924 readTextAppearance(context, appearance, attributes, false /* styleArray */);
925 attributes.mFontFamilyExplicit = false;
926 appearance.recycle();
927 }
928
929 boolean editable = getDefaultEditable();
930 CharSequence inputMethod = null;
931 int numeric = 0;
932 CharSequence digits = null;
933 boolean phone = false;
934 boolean autotext = false;
935 int autocap = -1;
936 int buffertype = 0;
937 boolean selectallonfocus = false;
938 Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
939 drawableBottom = null, drawableStart = null, drawableEnd = null;
940 ColorStateList drawableTint = null;
941 PorterDuff.Mode drawableTintMode = null;
942 int drawablePadding = 0;
943 int ellipsize = ELLIPSIZE_NOT_SET;
944 boolean singleLine = false;
945 int maxlength = -1;
946 CharSequence text = "";
947 CharSequence hint = null;
948 boolean password = false;
949 float autoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
950 float autoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
951 float autoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
952 int inputType = EditorInfo.TYPE_NULL;
953 a = theme.obtainStyledAttributes(
954 attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
Jeff Davidsona192cc22018-02-08 15:30:06 -0800955 int firstBaselineToTopHeight = -1;
956 int lastBaselineToBottomHeight = -1;
957 int lineHeight = -1;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400958
959 readTextAppearance(context, a, attributes, true /* styleArray */);
960
961 int n = a.getIndexCount();
962
Jeff Davidsona192cc22018-02-08 15:30:06 -0800963 // Must set id in a temporary variable because it will be reset by setText()
964 boolean textIsSetFromXml = false;
Justin Klaassen10d07c82017-09-15 17:58:39 -0400965 for (int i = 0; i < n; i++) {
966 int attr = a.getIndex(i);
967
968 switch (attr) {
969 case com.android.internal.R.styleable.TextView_editable:
970 editable = a.getBoolean(attr, editable);
971 break;
972
973 case com.android.internal.R.styleable.TextView_inputMethod:
974 inputMethod = a.getText(attr);
975 break;
976
977 case com.android.internal.R.styleable.TextView_numeric:
978 numeric = a.getInt(attr, numeric);
979 break;
980
981 case com.android.internal.R.styleable.TextView_digits:
982 digits = a.getText(attr);
983 break;
984
985 case com.android.internal.R.styleable.TextView_phoneNumber:
986 phone = a.getBoolean(attr, phone);
987 break;
988
989 case com.android.internal.R.styleable.TextView_autoText:
990 autotext = a.getBoolean(attr, autotext);
991 break;
992
993 case com.android.internal.R.styleable.TextView_capitalize:
994 autocap = a.getInt(attr, autocap);
995 break;
996
997 case com.android.internal.R.styleable.TextView_bufferType:
998 buffertype = a.getInt(attr, buffertype);
999 break;
1000
1001 case com.android.internal.R.styleable.TextView_selectAllOnFocus:
1002 selectallonfocus = a.getBoolean(attr, selectallonfocus);
1003 break;
1004
1005 case com.android.internal.R.styleable.TextView_autoLink:
1006 mAutoLinkMask = a.getInt(attr, 0);
1007 break;
1008
1009 case com.android.internal.R.styleable.TextView_linksClickable:
1010 mLinksClickable = a.getBoolean(attr, true);
1011 break;
1012
1013 case com.android.internal.R.styleable.TextView_drawableLeft:
1014 drawableLeft = a.getDrawable(attr);
1015 break;
1016
1017 case com.android.internal.R.styleable.TextView_drawableTop:
1018 drawableTop = a.getDrawable(attr);
1019 break;
1020
1021 case com.android.internal.R.styleable.TextView_drawableRight:
1022 drawableRight = a.getDrawable(attr);
1023 break;
1024
1025 case com.android.internal.R.styleable.TextView_drawableBottom:
1026 drawableBottom = a.getDrawable(attr);
1027 break;
1028
1029 case com.android.internal.R.styleable.TextView_drawableStart:
1030 drawableStart = a.getDrawable(attr);
1031 break;
1032
1033 case com.android.internal.R.styleable.TextView_drawableEnd:
1034 drawableEnd = a.getDrawable(attr);
1035 break;
1036
1037 case com.android.internal.R.styleable.TextView_drawableTint:
1038 drawableTint = a.getColorStateList(attr);
1039 break;
1040
1041 case com.android.internal.R.styleable.TextView_drawableTintMode:
1042 drawableTintMode = Drawable.parseTintMode(a.getInt(attr, -1), drawableTintMode);
1043 break;
1044
1045 case com.android.internal.R.styleable.TextView_drawablePadding:
1046 drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
1047 break;
1048
1049 case com.android.internal.R.styleable.TextView_maxLines:
1050 setMaxLines(a.getInt(attr, -1));
1051 break;
1052
1053 case com.android.internal.R.styleable.TextView_maxHeight:
1054 setMaxHeight(a.getDimensionPixelSize(attr, -1));
1055 break;
1056
1057 case com.android.internal.R.styleable.TextView_lines:
1058 setLines(a.getInt(attr, -1));
1059 break;
1060
1061 case com.android.internal.R.styleable.TextView_height:
1062 setHeight(a.getDimensionPixelSize(attr, -1));
1063 break;
1064
1065 case com.android.internal.R.styleable.TextView_minLines:
1066 setMinLines(a.getInt(attr, -1));
1067 break;
1068
1069 case com.android.internal.R.styleable.TextView_minHeight:
1070 setMinHeight(a.getDimensionPixelSize(attr, -1));
1071 break;
1072
1073 case com.android.internal.R.styleable.TextView_maxEms:
1074 setMaxEms(a.getInt(attr, -1));
1075 break;
1076
1077 case com.android.internal.R.styleable.TextView_maxWidth:
1078 setMaxWidth(a.getDimensionPixelSize(attr, -1));
1079 break;
1080
1081 case com.android.internal.R.styleable.TextView_ems:
1082 setEms(a.getInt(attr, -1));
1083 break;
1084
1085 case com.android.internal.R.styleable.TextView_width:
1086 setWidth(a.getDimensionPixelSize(attr, -1));
1087 break;
1088
1089 case com.android.internal.R.styleable.TextView_minEms:
1090 setMinEms(a.getInt(attr, -1));
1091 break;
1092
1093 case com.android.internal.R.styleable.TextView_minWidth:
1094 setMinWidth(a.getDimensionPixelSize(attr, -1));
1095 break;
1096
1097 case com.android.internal.R.styleable.TextView_gravity:
1098 setGravity(a.getInt(attr, -1));
1099 break;
1100
1101 case com.android.internal.R.styleable.TextView_hint:
1102 hint = a.getText(attr);
1103 break;
1104
1105 case com.android.internal.R.styleable.TextView_text:
Jeff Davidsona192cc22018-02-08 15:30:06 -08001106 textIsSetFromXml = true;
1107 mTextId = a.getResourceId(attr, ResourceId.ID_NULL);
Justin Klaassen10d07c82017-09-15 17:58:39 -04001108 text = a.getText(attr);
1109 break;
1110
1111 case com.android.internal.R.styleable.TextView_scrollHorizontally:
1112 if (a.getBoolean(attr, false)) {
1113 setHorizontallyScrolling(true);
1114 }
1115 break;
1116
1117 case com.android.internal.R.styleable.TextView_singleLine:
1118 singleLine = a.getBoolean(attr, singleLine);
1119 break;
1120
1121 case com.android.internal.R.styleable.TextView_ellipsize:
1122 ellipsize = a.getInt(attr, ellipsize);
1123 break;
1124
1125 case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
1126 setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
1127 break;
1128
1129 case com.android.internal.R.styleable.TextView_includeFontPadding:
1130 if (!a.getBoolean(attr, true)) {
1131 setIncludeFontPadding(false);
1132 }
1133 break;
1134
1135 case com.android.internal.R.styleable.TextView_cursorVisible:
1136 if (!a.getBoolean(attr, true)) {
1137 setCursorVisible(false);
1138 }
1139 break;
1140
1141 case com.android.internal.R.styleable.TextView_maxLength:
1142 maxlength = a.getInt(attr, -1);
1143 break;
1144
1145 case com.android.internal.R.styleable.TextView_textScaleX:
1146 setTextScaleX(a.getFloat(attr, 1.0f));
1147 break;
1148
1149 case com.android.internal.R.styleable.TextView_freezesText:
1150 mFreezesText = a.getBoolean(attr, false);
1151 break;
1152
1153 case com.android.internal.R.styleable.TextView_enabled:
1154 setEnabled(a.getBoolean(attr, isEnabled()));
1155 break;
1156
1157 case com.android.internal.R.styleable.TextView_password:
1158 password = a.getBoolean(attr, password);
1159 break;
1160
1161 case com.android.internal.R.styleable.TextView_lineSpacingExtra:
1162 mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
1163 break;
1164
1165 case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
1166 mSpacingMult = a.getFloat(attr, mSpacingMult);
1167 break;
1168
1169 case com.android.internal.R.styleable.TextView_inputType:
1170 inputType = a.getInt(attr, EditorInfo.TYPE_NULL);
1171 break;
1172
1173 case com.android.internal.R.styleable.TextView_allowUndo:
1174 createEditorIfNeeded();
1175 mEditor.mAllowUndo = a.getBoolean(attr, true);
1176 break;
1177
1178 case com.android.internal.R.styleable.TextView_imeOptions:
1179 createEditorIfNeeded();
1180 mEditor.createInputContentTypeIfNeeded();
1181 mEditor.mInputContentType.imeOptions = a.getInt(attr,
1182 mEditor.mInputContentType.imeOptions);
1183 break;
1184
1185 case com.android.internal.R.styleable.TextView_imeActionLabel:
1186 createEditorIfNeeded();
1187 mEditor.createInputContentTypeIfNeeded();
1188 mEditor.mInputContentType.imeActionLabel = a.getText(attr);
1189 break;
1190
1191 case com.android.internal.R.styleable.TextView_imeActionId:
1192 createEditorIfNeeded();
1193 mEditor.createInputContentTypeIfNeeded();
1194 mEditor.mInputContentType.imeActionId = a.getInt(attr,
1195 mEditor.mInputContentType.imeActionId);
1196 break;
1197
1198 case com.android.internal.R.styleable.TextView_privateImeOptions:
1199 setPrivateImeOptions(a.getString(attr));
1200 break;
1201
1202 case com.android.internal.R.styleable.TextView_editorExtras:
1203 try {
1204 setInputExtras(a.getResourceId(attr, 0));
1205 } catch (XmlPullParserException e) {
1206 Log.w(LOG_TAG, "Failure reading input extras", e);
1207 } catch (IOException e) {
1208 Log.w(LOG_TAG, "Failure reading input extras", e);
1209 }
1210 break;
1211
1212 case com.android.internal.R.styleable.TextView_textCursorDrawable:
1213 mCursorDrawableRes = a.getResourceId(attr, 0);
1214 break;
1215
1216 case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
1217 mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
1218 break;
1219
1220 case com.android.internal.R.styleable.TextView_textSelectHandleRight:
1221 mTextSelectHandleRightRes = a.getResourceId(attr, 0);
1222 break;
1223
1224 case com.android.internal.R.styleable.TextView_textSelectHandle:
1225 mTextSelectHandleRes = a.getResourceId(attr, 0);
1226 break;
1227
1228 case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
1229 mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
1230 break;
1231
1232 case com.android.internal.R.styleable.TextView_textEditSuggestionContainerLayout:
1233 mTextEditSuggestionContainerLayout = a.getResourceId(attr, 0);
1234 break;
1235
1236 case com.android.internal.R.styleable.TextView_textEditSuggestionHighlightStyle:
1237 mTextEditSuggestionHighlightStyle = a.getResourceId(attr, 0);
1238 break;
1239
1240 case com.android.internal.R.styleable.TextView_textIsSelectable:
1241 setTextIsSelectable(a.getBoolean(attr, false));
1242 break;
1243
1244 case com.android.internal.R.styleable.TextView_breakStrategy:
1245 mBreakStrategy = a.getInt(attr, Layout.BREAK_STRATEGY_SIMPLE);
1246 break;
1247
1248 case com.android.internal.R.styleable.TextView_hyphenationFrequency:
1249 mHyphenationFrequency = a.getInt(attr, Layout.HYPHENATION_FREQUENCY_NONE);
1250 break;
1251
1252 case com.android.internal.R.styleable.TextView_autoSizeTextType:
1253 mAutoSizeTextType = a.getInt(attr, AUTO_SIZE_TEXT_TYPE_NONE);
1254 break;
1255
1256 case com.android.internal.R.styleable.TextView_autoSizeStepGranularity:
1257 autoSizeStepGranularityInPx = a.getDimension(attr,
1258 UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
1259 break;
1260
1261 case com.android.internal.R.styleable.TextView_autoSizeMinTextSize:
1262 autoSizeMinTextSizeInPx = a.getDimension(attr,
1263 UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
1264 break;
1265
1266 case com.android.internal.R.styleable.TextView_autoSizeMaxTextSize:
1267 autoSizeMaxTextSizeInPx = a.getDimension(attr,
1268 UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
1269 break;
1270
1271 case com.android.internal.R.styleable.TextView_autoSizePresetSizes:
1272 final int autoSizeStepSizeArrayResId = a.getResourceId(attr, 0);
1273 if (autoSizeStepSizeArrayResId > 0) {
1274 final TypedArray autoSizePresetTextSizes = a.getResources()
1275 .obtainTypedArray(autoSizeStepSizeArrayResId);
1276 setupAutoSizeUniformPresetSizes(autoSizePresetTextSizes);
1277 autoSizePresetTextSizes.recycle();
1278 }
1279 break;
1280 case com.android.internal.R.styleable.TextView_justificationMode:
1281 mJustificationMode = a.getInt(attr, Layout.JUSTIFICATION_MODE_NONE);
1282 break;
Jeff Davidsona192cc22018-02-08 15:30:06 -08001283
1284 case com.android.internal.R.styleable.TextView_firstBaselineToTopHeight:
1285 firstBaselineToTopHeight = a.getDimensionPixelSize(attr, -1);
1286 break;
1287
1288 case com.android.internal.R.styleable.TextView_lastBaselineToBottomHeight:
1289 lastBaselineToBottomHeight = a.getDimensionPixelSize(attr, -1);
1290 break;
1291
1292 case com.android.internal.R.styleable.TextView_lineHeight:
1293 lineHeight = a.getDimensionPixelSize(attr, -1);
1294 break;
Justin Klaassen10d07c82017-09-15 17:58:39 -04001295 }
1296 }
1297
1298 a.recycle();
1299
1300 BufferType bufferType = BufferType.EDITABLE;
1301
1302 final int variation =
1303 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
1304 final boolean passwordInputType = variation
1305 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
1306 final boolean webPasswordInputType = variation
1307 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
1308 final boolean numberPasswordInputType = variation
1309 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
1310
1311 final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
1312 mUseInternationalizedInput = targetSdkVersion >= VERSION_CODES.O;
Justin Klaassen93b7ee42017-10-10 15:20:13 -04001313 mUseFallbackLineSpacing = targetSdkVersion >= VERSION_CODES.P;
Justin Klaassen10d07c82017-09-15 17:58:39 -04001314
1315 if (inputMethod != null) {
1316 Class<?> c;
1317
1318 try {
1319 c = Class.forName(inputMethod.toString());
1320 } catch (ClassNotFoundException ex) {
1321 throw new RuntimeException(ex);
1322 }
1323
1324 try {
1325 createEditorIfNeeded();
1326 mEditor.mKeyListener = (KeyListener) c.newInstance();
1327 } catch (InstantiationException ex) {
1328 throw new RuntimeException(ex);
1329 } catch (IllegalAccessException ex) {
1330 throw new RuntimeException(ex);
1331 }
1332 try {
1333 mEditor.mInputType = inputType != EditorInfo.TYPE_NULL
1334 ? inputType
1335 : mEditor.mKeyListener.getInputType();
1336 } catch (IncompatibleClassChangeError e) {
1337 mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
1338 }
1339 } else if (digits != null) {
1340 createEditorIfNeeded();
1341 mEditor.mKeyListener = DigitsKeyListener.getInstance(digits.toString());
1342 // If no input type was specified, we will default to generic
1343 // text, since we can't tell the IME about the set of digits
1344 // that was selected.
1345 mEditor.mInputType = inputType != EditorInfo.TYPE_NULL
1346 ? inputType : EditorInfo.TYPE_CLASS_TEXT;
1347 } else if (inputType != EditorInfo.TYPE_NULL) {
1348 setInputType(inputType, true);
1349 // If set, the input type overrides what was set using the deprecated singleLine flag.
1350 singleLine = !isMultilineInputType(inputType);
1351 } else if (phone) {
1352 createEditorIfNeeded();
1353 mEditor.mKeyListener = DialerKeyListener.getInstance();
1354 mEditor.mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
1355 } else if (numeric != 0) {
1356 createEditorIfNeeded();
1357 mEditor.mKeyListener = DigitsKeyListener.getInstance(
1358 null, // locale
1359 (numeric & SIGNED) != 0,
1360 (numeric & DECIMAL) != 0);
1361 inputType = mEditor.mKeyListener.getInputType();
1362 mEditor.mInputType = inputType;
1363 } else if (autotext || autocap != -1) {
1364 TextKeyListener.Capitalize cap;
1365
1366 inputType = EditorInfo.TYPE_CLASS_TEXT;
1367
1368 switch (autocap) {
1369 case 1:
1370 cap = TextKeyListener.Capitalize.SENTENCES;
1371 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
1372 break;
1373
1374 case 2:
1375 cap = TextKeyListener.Capitalize.WORDS;
1376 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
1377 break;
1378
1379 case 3:
1380 cap = TextKeyListener.Capitalize.CHARACTERS;
1381 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
1382 break;
1383
1384 default:
1385 cap = TextKeyListener.Capitalize.NONE;
1386 break;
1387 }
1388
1389 createEditorIfNeeded();
1390 mEditor.mKeyListener = TextKeyListener.getInstance(autotext, cap);
1391 mEditor.mInputType = inputType;
1392 } else if (editable) {
1393 createEditorIfNeeded();
1394 mEditor.mKeyListener = TextKeyListener.getInstance();
1395 mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
1396 } else if (isTextSelectable()) {
1397 // Prevent text changes from keyboard.
1398 if (mEditor != null) {
1399 mEditor.mKeyListener = null;
1400 mEditor.mInputType = EditorInfo.TYPE_NULL;
1401 }
1402 bufferType = BufferType.SPANNABLE;
1403 // So that selection can be changed using arrow keys and touch is handled.
1404 setMovementMethod(ArrowKeyMovementMethod.getInstance());
1405 } else {
1406 if (mEditor != null) mEditor.mKeyListener = null;
1407
1408 switch (buffertype) {
1409 case 0:
1410 bufferType = BufferType.NORMAL;
1411 break;
1412 case 1:
1413 bufferType = BufferType.SPANNABLE;
1414 break;
1415 case 2:
1416 bufferType = BufferType.EDITABLE;
1417 break;
1418 }
1419 }
1420
1421 if (mEditor != null) {
1422 mEditor.adjustInputType(password, passwordInputType, webPasswordInputType,
1423 numberPasswordInputType);
1424 }
1425
1426 if (selectallonfocus) {
1427 createEditorIfNeeded();
1428 mEditor.mSelectAllOnFocus = true;
1429
1430 if (bufferType == BufferType.NORMAL) {
1431 bufferType = BufferType.SPANNABLE;
1432 }
1433 }
1434
1435 // Set up the tint (if needed) before setting the drawables so that it
1436 // gets applied correctly.
1437 if (drawableTint != null || drawableTintMode != null) {
1438 if (mDrawables == null) {
1439 mDrawables = new Drawables(context);
1440 }
1441 if (drawableTint != null) {
1442 mDrawables.mTintList = drawableTint;
1443 mDrawables.mHasTint = true;
1444 }
1445 if (drawableTintMode != null) {
1446 mDrawables.mTintMode = drawableTintMode;
1447 mDrawables.mHasTintMode = true;
1448 }
1449 }
1450
1451 // This call will save the initial left/right drawables
1452 setCompoundDrawablesWithIntrinsicBounds(
1453 drawableLeft, drawableTop, drawableRight, drawableBottom);
1454 setRelativeDrawablesIfNeeded(drawableStart, drawableEnd);
1455 setCompoundDrawablePadding(drawablePadding);
1456
1457 // Same as setSingleLine(), but make sure the transformation method and the maximum number
1458 // of lines of height are unchanged for multi-line TextViews.
1459 setInputTypeSingleLine(singleLine);
1460 applySingleLine(singleLine, singleLine, singleLine);
1461
1462 if (singleLine && getKeyListener() == null && ellipsize == ELLIPSIZE_NOT_SET) {
1463 ellipsize = ELLIPSIZE_END;
1464 }
1465
1466 switch (ellipsize) {
1467 case ELLIPSIZE_START:
1468 setEllipsize(TextUtils.TruncateAt.START);
1469 break;
1470 case ELLIPSIZE_MIDDLE:
1471 setEllipsize(TextUtils.TruncateAt.MIDDLE);
1472 break;
1473 case ELLIPSIZE_END:
1474 setEllipsize(TextUtils.TruncateAt.END);
1475 break;
1476 case ELLIPSIZE_MARQUEE:
1477 if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
1478 setHorizontalFadingEdgeEnabled(true);
1479 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
1480 } else {
1481 setHorizontalFadingEdgeEnabled(false);
1482 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
1483 }
1484 setEllipsize(TextUtils.TruncateAt.MARQUEE);
1485 break;
1486 }
1487
1488 final boolean isPassword = password || passwordInputType || webPasswordInputType
1489 || numberPasswordInputType;
1490 final boolean isMonospaceEnforced = isPassword || (mEditor != null
1491 && (mEditor.mInputType
1492 & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
1493 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD));
1494 if (isMonospaceEnforced) {
1495 attributes.mTypefaceIndex = MONOSPACE;
1496 }
1497
1498 applyTextAppearance(attributes);
1499
1500 if (isPassword) {
1501 setTransformationMethod(PasswordTransformationMethod.getInstance());
1502 }
1503
1504 if (maxlength >= 0) {
1505 setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
1506 } else {
1507 setFilters(NO_FILTERS);
1508 }
1509
1510 setText(text, bufferType);
Jeff Davidsona192cc22018-02-08 15:30:06 -08001511 if (textIsSetFromXml) {
1512 mTextSetFromXmlOrResourceId = true;
Justin Klaassen10d07c82017-09-15 17:58:39 -04001513 }
1514
1515 if (hint != null) setHint(hint);
1516
1517 /*
1518 * Views are not normally clickable unless specified to be.
1519 * However, TextViews that have input or movement methods *are*
1520 * clickable by default. By setting clickable here, we implicitly set focusable as well
1521 * if not overridden by the developer.
1522 */
1523 a = context.obtainStyledAttributes(
1524 attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
1525 boolean canInputOrMove = (mMovement != null || getKeyListener() != null);
1526 boolean clickable = canInputOrMove || isClickable();
1527 boolean longClickable = canInputOrMove || isLongClickable();
1528 int focusable = getFocusable();
1529
1530 n = a.getIndexCount();
1531 for (int i = 0; i < n; i++) {
1532 int attr = a.getIndex(i);
1533
1534 switch (attr) {
1535 case com.android.internal.R.styleable.View_focusable:
1536 TypedValue val = new TypedValue();
1537 if (a.getValue(attr, val)) {
1538 focusable = (val.type == TypedValue.TYPE_INT_BOOLEAN)
1539 ? (val.data == 0 ? NOT_FOCUSABLE : FOCUSABLE)
1540 : val.data;
1541 }
1542 break;
1543
1544 case com.android.internal.R.styleable.View_clickable:
1545 clickable = a.getBoolean(attr, clickable);
1546 break;
1547
1548 case com.android.internal.R.styleable.View_longClickable:
1549 longClickable = a.getBoolean(attr, longClickable);
1550 break;
1551 }
1552 }
1553 a.recycle();
1554
1555 // Some apps were relying on the undefined behavior of focusable winning over
1556 // focusableInTouchMode != focusable in TextViews if both were specified in XML (usually
1557 // when starting with EditText and setting only focusable=false). To keep those apps from
1558 // breaking, re-apply the focusable attribute here.
1559 if (focusable != getFocusable()) {
1560 setFocusable(focusable);
1561 }
1562 setClickable(clickable);
1563 setLongClickable(longClickable);
1564
1565 if (mEditor != null) mEditor.prepareCursorControllers();
1566
1567 // If not explicitly specified this view is important for accessibility.
1568 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
1569 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
1570 }
1571
1572 if (supportsAutoSizeText()) {
1573 if (mAutoSizeTextType == AUTO_SIZE_TEXT_TYPE_UNIFORM) {
1574 // If uniform auto-size has been specified but preset values have not been set then
1575 // replace the auto-size configuration values that have not been specified with the
1576 // defaults.
1577 if (!mHasPresetAutoSizeValues) {
1578 final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
1579
1580 if (autoSizeMinTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
1581 autoSizeMinTextSizeInPx = TypedValue.applyDimension(
1582 TypedValue.COMPLEX_UNIT_SP,
1583 DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP,
1584 displayMetrics);
1585 }
1586
1587 if (autoSizeMaxTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
1588 autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
1589 TypedValue.COMPLEX_UNIT_SP,
1590 DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP,
1591 displayMetrics);
1592 }
1593
1594 if (autoSizeStepGranularityInPx
1595 == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
1596 autoSizeStepGranularityInPx = DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX;
1597 }
1598
1599 validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
1600 autoSizeMaxTextSizeInPx,
1601 autoSizeStepGranularityInPx);
1602 }
1603
1604 setupAutoSizeText();
1605 }
1606 } else {
1607 mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
1608 }
Jeff Davidsona192cc22018-02-08 15:30:06 -08001609
1610 if (firstBaselineToTopHeight >= 0) {
1611 setFirstBaselineToTopHeight(firstBaselineToTopHeight);
1612 }
1613 if (lastBaselineToBottomHeight >= 0) {
1614 setLastBaselineToBottomHeight(lastBaselineToBottomHeight);
1615 }
1616 if (lineHeight >= 0) {
1617 setLineHeight(lineHeight);
1618 }
Justin Klaassen10d07c82017-09-15 17:58:39 -04001619 }
1620
Justin Klaassen4d01eea2018-04-03 23:21:57 -04001621 // Update mText and mPrecomputed
1622 private void setTextInternal(@Nullable CharSequence text) {
1623 mText = text;
1624 mSpannable = (text instanceof Spannable) ? (Spannable) text : null;
1625 mPrecomputed = (text instanceof PrecomputedText) ? (PrecomputedText) text : null;
1626 }
1627
Justin Klaassen10d07c82017-09-15 17:58:39 -04001628 /**
1629 * Specify whether this widget should automatically scale the text to try to perfectly fit
1630 * within the layout bounds by using the default auto-size configuration.
1631 *
1632 * @param autoSizeTextType the type of auto-size. Must be one of
1633 * {@link TextView#AUTO_SIZE_TEXT_TYPE_NONE} or
1634 * {@link TextView#AUTO_SIZE_TEXT_TYPE_UNIFORM}
1635 *
1636 * @throws IllegalArgumentException if <code>autoSizeTextType</code> is none of the types above.
1637 *
1638 * @attr ref android.R.styleable#TextView_autoSizeTextType
1639 *
1640 * @see #getAutoSizeTextType()
1641 */
1642 public void setAutoSizeTextTypeWithDefaults(@AutoSizeTextType int autoSizeTextType) {
1643 if (supportsAutoSizeText()) {
1644 switch (autoSizeTextType) {
1645 case AUTO_SIZE_TEXT_TYPE_NONE:
1646 clearAutoSizeConfiguration();
1647 break;
1648 case AUTO_SIZE_TEXT_TYPE_UNIFORM:
1649 final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
1650 final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
1651 TypedValue.COMPLEX_UNIT_SP,
1652 DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP,
1653 displayMetrics);
1654 final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
1655 TypedValue.COMPLEX_UNIT_SP,
1656 DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP,
1657 displayMetrics);
1658
1659 validateAndSetAutoSizeTextTypeUniformConfiguration(
1660 autoSizeMinTextSizeInPx,
1661 autoSizeMaxTextSizeInPx,
1662 DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX);
1663 if (setupAutoSizeText()) {
1664 autoSizeText();
1665 invalidate();
1666 }
1667 break;
1668 default:
1669 throw new IllegalArgumentException(
1670 "Unknown auto-size text type: " + autoSizeTextType);
1671 }
1672 }
1673 }
1674
1675 /**
1676 * Specify whether this widget should automatically scale the text to try to perfectly fit
1677 * within the layout bounds. If all the configuration params are valid the type of auto-size is
1678 * set to {@link #AUTO_SIZE_TEXT_TYPE_UNIFORM}.
1679 *
1680 * @param autoSizeMinTextSize the minimum text size available for auto-size
1681 * @param autoSizeMaxTextSize the maximum text size available for auto-size
1682 * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with
1683 * the minimum and maximum text size in order to build the set of
1684 * text sizes the system uses to choose from when auto-sizing
1685 * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the
1686 * possible dimension units
1687 *
1688 * @throws IllegalArgumentException if any of the configuration params are invalid.
1689 *
1690 * @attr ref android.R.styleable#TextView_autoSizeTextType
1691 * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
1692 * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
1693 * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
1694 *
1695 * @see #setAutoSizeTextTypeWithDefaults(int)
1696 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
1697 * @see #getAutoSizeMinTextSize()
1698 * @see #getAutoSizeMaxTextSize()
1699 * @see #getAutoSizeStepGranularity()
1700 * @see #getAutoSizeTextAvailableSizes()
1701 */
1702 public void setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize,
1703 int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit) {
1704 if (supportsAutoSizeText()) {
1705 final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
1706 final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
1707 unit, autoSizeMinTextSize, displayMetrics);
1708 final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
1709 unit, autoSizeMaxTextSize, displayMetrics);
1710 final float autoSizeStepGranularityInPx = TypedValue.applyDimension(
1711 unit, autoSizeStepGranularity, displayMetrics);
1712
1713 validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
1714 autoSizeMaxTextSizeInPx,
1715 autoSizeStepGranularityInPx);
1716
1717 if (setupAutoSizeText()) {
1718 autoSizeText();
1719 invalidate();
1720 }
1721 }
1722 }
1723
1724 /**
1725 * Specify whether this widget should automatically scale the text to try to perfectly fit
1726 * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid
1727 * then the type of auto-size is set to {@link #AUTO_SIZE_TEXT_TYPE_UNIFORM}.
1728 *
1729 * @param presetSizes an {@code int} array of sizes in pixels
1730 * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for
1731 * the possible dimension units
1732 *
1733 * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid.
1734 *
1735 * @attr ref android.R.styleable#TextView_autoSizeTextType
1736 * @attr ref android.R.styleable#TextView_autoSizePresetSizes
1737 *
1738 * @see #setAutoSizeTextTypeWithDefaults(int)
1739 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
1740 * @see #getAutoSizeMinTextSize()
1741 * @see #getAutoSizeMaxTextSize()
1742 * @see #getAutoSizeTextAvailableSizes()
1743 */
1744 public void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit) {
1745 if (supportsAutoSizeText()) {
1746 final int presetSizesLength = presetSizes.length;
1747 if (presetSizesLength > 0) {
1748 int[] presetSizesInPx = new int[presetSizesLength];
1749
1750 if (unit == TypedValue.COMPLEX_UNIT_PX) {
1751 presetSizesInPx = Arrays.copyOf(presetSizes, presetSizesLength);
1752 } else {
1753 final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
1754 // Convert all to sizes to pixels.
1755 for (int i = 0; i < presetSizesLength; i++) {
1756 presetSizesInPx[i] = Math.round(TypedValue.applyDimension(unit,
1757 presetSizes[i], displayMetrics));
1758 }
1759 }
1760
1761 mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(presetSizesInPx);
1762 if (!setupAutoSizeUniformPresetSizesConfiguration()) {
1763 throw new IllegalArgumentException("None of the preset sizes is valid: "
1764 + Arrays.toString(presetSizes));
1765 }
1766 } else {
1767 mHasPresetAutoSizeValues = false;
1768 }
1769
1770 if (setupAutoSizeText()) {
1771 autoSizeText();
1772 invalidate();
1773 }
1774 }
1775 }
1776
1777 /**
1778 * Returns the type of auto-size set for this widget.
1779 *
1780 * @return an {@code int} corresponding to one of the auto-size types:
1781 * {@link TextView#AUTO_SIZE_TEXT_TYPE_NONE} or
1782 * {@link TextView#AUTO_SIZE_TEXT_TYPE_UNIFORM}
1783 *
1784 * @attr ref android.R.styleable#TextView_autoSizeTextType
1785 *
1786 * @see #setAutoSizeTextTypeWithDefaults(int)
1787 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
1788 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
1789 */
1790 @AutoSizeTextType
1791 public int getAutoSizeTextType() {
1792 return mAutoSizeTextType;
1793 }
1794
1795 /**
1796 * @return the current auto-size step granularity in pixels.
1797 *
1798 * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
1799 *
1800 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
1801 */
1802 public int getAutoSizeStepGranularity() {
1803 return Math.round(mAutoSizeStepGranularityInPx);
1804 }
1805
1806 /**
1807 * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that
1808 * if auto-size has not been configured this function returns {@code -1}.
1809 *
1810 * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
1811 *
1812 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
1813 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
1814 */
1815 public int getAutoSizeMinTextSize() {
1816 return Math.round(mAutoSizeMinTextSizeInPx);
1817 }
1818
1819 /**
1820 * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that
1821 * if auto-size has not been configured this function returns {@code -1}.
1822 *
1823 * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
1824 *
1825 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
1826 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
1827 */
1828 public int getAutoSizeMaxTextSize() {
1829 return Math.round(mAutoSizeMaxTextSizeInPx);
1830 }
1831
1832 /**
1833 * @return the current auto-size {@code int} sizes array (in pixels).
1834 *
1835 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
1836 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
1837 */
1838 public int[] getAutoSizeTextAvailableSizes() {
1839 return mAutoSizeTextSizesInPx;
1840 }
1841
1842 private void setupAutoSizeUniformPresetSizes(TypedArray textSizes) {
1843 final int textSizesLength = textSizes.length();
1844 final int[] parsedSizes = new int[textSizesLength];
1845
1846 if (textSizesLength > 0) {
1847 for (int i = 0; i < textSizesLength; i++) {
1848 parsedSizes[i] = textSizes.getDimensionPixelSize(i, -1);
1849 }
1850 mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(parsedSizes);
1851 setupAutoSizeUniformPresetSizesConfiguration();
1852 }
1853 }
1854
1855 private boolean setupAutoSizeUniformPresetSizesConfiguration() {
1856 final int sizesLength = mAutoSizeTextSizesInPx.length;
1857 mHasPresetAutoSizeValues = sizesLength > 0;
1858 if (mHasPresetAutoSizeValues) {
1859 mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_UNIFORM;
1860 mAutoSizeMinTextSizeInPx = mAutoSizeTextSizesInPx[0];
1861 mAutoSizeMaxTextSizeInPx = mAutoSizeTextSizesInPx[sizesLength - 1];
1862 mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
1863 }
1864 return mHasPresetAutoSizeValues;
1865 }
1866
1867 /**
1868 * If all params are valid then save the auto-size configuration.
1869 *
1870 * @throws IllegalArgumentException if any of the params are invalid
1871 */
1872 private void validateAndSetAutoSizeTextTypeUniformConfiguration(float autoSizeMinTextSizeInPx,
1873 float autoSizeMaxTextSizeInPx, float autoSizeStepGranularityInPx) {
1874 // First validate.
1875 if (autoSizeMinTextSizeInPx <= 0) {
1876 throw new IllegalArgumentException("Minimum auto-size text size ("
1877 + autoSizeMinTextSizeInPx + "px) is less or equal to (0px)");
1878 }
1879
1880 if (autoSizeMaxTextSizeInPx <= autoSizeMinTextSizeInPx) {
1881 throw new IllegalArgumentException("Maximum auto-size text size ("
1882 + autoSizeMaxTextSizeInPx + "px) is less or equal to minimum auto-size "
1883 + "text size (" + autoSizeMinTextSizeInPx + "px)");
1884 }
1885
1886 if (autoSizeStepGranularityInPx <= 0) {
1887 throw new IllegalArgumentException("The auto-size step granularity ("
1888 + autoSizeStepGranularityInPx + "px) is less or equal to (0px)");
1889 }
1890
1891 // All good, persist the configuration.
1892 mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_UNIFORM;
1893 mAutoSizeMinTextSizeInPx = autoSizeMinTextSizeInPx;
1894 mAutoSizeMaxTextSizeInPx = autoSizeMaxTextSizeInPx;
1895 mAutoSizeStepGranularityInPx = autoSizeStepGranularityInPx;
1896 mHasPresetAutoSizeValues = false;
1897 }
1898
1899 private void clearAutoSizeConfiguration() {
1900 mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
1901 mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
1902 mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
1903 mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
1904 mAutoSizeTextSizesInPx = EmptyArray.INT;
1905 mNeedsAutoSizeText = false;
1906 }
1907
1908 // Returns distinct sorted positive values.
1909 private int[] cleanupAutoSizePresetSizes(int[] presetValues) {
1910 final int presetValuesLength = presetValues.length;
1911 if (presetValuesLength == 0) {
1912 return presetValues;
1913 }
1914 Arrays.sort(presetValues);
1915
1916 final IntArray uniqueValidSizes = new IntArray();
1917 for (int i = 0; i < presetValuesLength; i++) {
1918 final int currentPresetValue = presetValues[i];
1919
1920 if (currentPresetValue > 0
1921 && uniqueValidSizes.binarySearch(currentPresetValue) < 0) {
1922 uniqueValidSizes.add(currentPresetValue);
1923 }
1924 }
1925
1926 return presetValuesLength == uniqueValidSizes.size()
1927 ? presetValues
1928 : uniqueValidSizes.toArray();
1929 }
1930
1931 private boolean setupAutoSizeText() {
1932 if (supportsAutoSizeText() && mAutoSizeTextType == AUTO_SIZE_TEXT_TYPE_UNIFORM) {
1933 // Calculate the sizes set based on minimum size, maximum size and step size if we do
1934 // not have a predefined set of sizes or if the current sizes array is empty.
1935 if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04001936 final int autoSizeValuesLength = ((int) Math.floor((mAutoSizeMaxTextSizeInPx
1937 - mAutoSizeMinTextSizeInPx) / mAutoSizeStepGranularityInPx)) + 1;
1938 final int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength];
Justin Klaassen10d07c82017-09-15 17:58:39 -04001939 for (int i = 0; i < autoSizeValuesLength; i++) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04001940 autoSizeTextSizesInPx[i] = Math.round(
1941 mAutoSizeMinTextSizeInPx + (i * mAutoSizeStepGranularityInPx));
Justin Klaassen10d07c82017-09-15 17:58:39 -04001942 }
1943 mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx);
1944 }
1945
1946 mNeedsAutoSizeText = true;
1947 } else {
1948 mNeedsAutoSizeText = false;
1949 }
1950
1951 return mNeedsAutoSizeText;
1952 }
1953
1954 private int[] parseDimensionArray(TypedArray dimens) {
1955 if (dimens == null) {
1956 return null;
1957 }
1958 int[] result = new int[dimens.length()];
1959 for (int i = 0; i < result.length; i++) {
1960 result[i] = dimens.getDimensionPixelSize(i, 0);
1961 }
1962 return result;
1963 }
1964
1965 /**
1966 * @hide
1967 */
1968 @Override
1969 public void onActivityResult(int requestCode, int resultCode, Intent data) {
1970 if (requestCode == PROCESS_TEXT_REQUEST_CODE) {
1971 if (resultCode == Activity.RESULT_OK && data != null) {
1972 CharSequence result = data.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT);
1973 if (result != null) {
1974 if (isTextEditable()) {
1975 replaceSelectionWithText(result);
1976 if (mEditor != null) {
1977 mEditor.refreshTextActionMode();
1978 }
1979 } else {
1980 if (result.length() > 0) {
1981 Toast.makeText(getContext(), String.valueOf(result), Toast.LENGTH_LONG)
1982 .show();
1983 }
1984 }
1985 }
Justin Klaassen4d01eea2018-04-03 23:21:57 -04001986 } else if (mSpannable != null) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04001987 // Reset the selection.
Justin Klaassen4d01eea2018-04-03 23:21:57 -04001988 Selection.setSelection(mSpannable, getSelectionEnd());
Justin Klaassen10d07c82017-09-15 17:58:39 -04001989 }
1990 }
1991 }
1992
Justin Klaassen4d01eea2018-04-03 23:21:57 -04001993 /**
1994 * Sets the Typeface taking into account the given attributes.
1995 *
1996 * @param typeface a typeface
1997 * @param familyName family name string, e.g. "serif"
1998 * @param typefaceIndex an index of the typeface enum, e.g. SANS, SERIF.
1999 * @param style a typeface style
2000 * @param weight a weight value for the Typeface or -1 if not specified.
2001 */
2002 private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
2003 @XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
2004 @IntRange(from = -1, to = Typeface.MAX_WEIGHT) int weight) {
2005 if (typeface == null && familyName != null) {
2006 // Lookup normal Typeface from system font map.
2007 final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
2008 resolveStyleAndSetTypeface(normalTypeface, style, weight);
2009 } else if (typeface != null) {
2010 resolveStyleAndSetTypeface(typeface, style, weight);
2011 } else { // both typeface and familyName is null.
2012 switch (typefaceIndex) {
2013 case SANS:
2014 resolveStyleAndSetTypeface(Typeface.SANS_SERIF, style, weight);
2015 break;
2016 case SERIF:
2017 resolveStyleAndSetTypeface(Typeface.SERIF, style, weight);
2018 break;
2019 case MONOSPACE:
2020 resolveStyleAndSetTypeface(Typeface.MONOSPACE, style, weight);
2021 break;
2022 case DEFAULT_TYPEFACE:
2023 default:
2024 resolveStyleAndSetTypeface(null, style, weight);
2025 break;
2026 }
Justin Klaassen10d07c82017-09-15 17:58:39 -04002027 }
Justin Klaassen4d01eea2018-04-03 23:21:57 -04002028 }
2029
2030 private void resolveStyleAndSetTypeface(@NonNull Typeface typeface, @Typeface.Style int style,
2031 @IntRange(from = -1, to = Typeface.MAX_WEIGHT) int weight) {
2032 if (weight >= 0) {
2033 weight = Math.min(Typeface.MAX_WEIGHT, weight);
2034 final boolean italic = (style & Typeface.ITALIC) != 0;
2035 setTypeface(Typeface.create(typeface, weight, italic));
2036 } else {
2037 setTypeface(typeface, style);
Justin Klaassen10d07c82017-09-15 17:58:39 -04002038 }
Justin Klaassen10d07c82017-09-15 17:58:39 -04002039 }
2040
2041 private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) {
2042 boolean hasRelativeDrawables = (start != null) || (end != null);
2043 if (hasRelativeDrawables) {
2044 Drawables dr = mDrawables;
2045 if (dr == null) {
2046 mDrawables = dr = new Drawables(getContext());
2047 }
2048 mDrawables.mOverride = true;
2049 final Rect compoundRect = dr.mCompoundRect;
2050 int[] state = getDrawableState();
2051 if (start != null) {
2052 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
2053 start.setState(state);
2054 start.copyBounds(compoundRect);
2055 start.setCallback(this);
2056
2057 dr.mDrawableStart = start;
2058 dr.mDrawableSizeStart = compoundRect.width();
2059 dr.mDrawableHeightStart = compoundRect.height();
2060 } else {
2061 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
2062 }
2063 if (end != null) {
2064 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
2065 end.setState(state);
2066 end.copyBounds(compoundRect);
2067 end.setCallback(this);
2068
2069 dr.mDrawableEnd = end;
2070 dr.mDrawableSizeEnd = compoundRect.width();
2071 dr.mDrawableHeightEnd = compoundRect.height();
2072 } else {
2073 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
2074 }
2075 resetResolvedDrawables();
2076 resolveDrawables();
2077 applyCompoundDrawableTint();
2078 }
2079 }
2080
2081 @android.view.RemotableViewMethod
2082 @Override
2083 public void setEnabled(boolean enabled) {
2084 if (enabled == isEnabled()) {
2085 return;
2086 }
2087
2088 if (!enabled) {
2089 // Hide the soft input if the currently active TextView is disabled
2090 InputMethodManager imm = InputMethodManager.peekInstance();
2091 if (imm != null && imm.isActive(this)) {
2092 imm.hideSoftInputFromWindow(getWindowToken(), 0);
2093 }
2094 }
2095
2096 super.setEnabled(enabled);
2097
2098 if (enabled) {
2099 // Make sure IME is updated with current editor info.
2100 InputMethodManager imm = InputMethodManager.peekInstance();
2101 if (imm != null) imm.restartInput(this);
2102 }
2103
2104 // Will change text color
2105 if (mEditor != null) {
2106 mEditor.invalidateTextDisplayList();
2107 mEditor.prepareCursorControllers();
2108
2109 // start or stop the cursor blinking as appropriate
2110 mEditor.makeBlink();
2111 }
2112 }
2113
2114 /**
2115 * Sets the typeface and style in which the text should be displayed,
2116 * and turns on the fake bold and italic bits in the Paint if the
2117 * Typeface that you provided does not have all the bits in the
2118 * style that you specified.
2119 *
2120 * @attr ref android.R.styleable#TextView_typeface
2121 * @attr ref android.R.styleable#TextView_textStyle
2122 */
Justin Klaassen4d01eea2018-04-03 23:21:57 -04002123 public void setTypeface(@Nullable Typeface tf, @Typeface.Style int style) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04002124 if (style > 0) {
2125 if (tf == null) {
2126 tf = Typeface.defaultFromStyle(style);
2127 } else {
2128 tf = Typeface.create(tf, style);
2129 }
2130
2131 setTypeface(tf);
2132 // now compute what (if any) algorithmic styling is needed
2133 int typefaceStyle = tf != null ? tf.getStyle() : 0;
2134 int need = style & ~typefaceStyle;
2135 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
2136 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
2137 } else {
2138 mTextPaint.setFakeBoldText(false);
2139 mTextPaint.setTextSkewX(0);
2140 setTypeface(tf);
2141 }
2142 }
2143
2144 /**
2145 * Subclasses override this to specify that they have a KeyListener
2146 * by default even if not specifically called for in the XML options.
2147 */
2148 protected boolean getDefaultEditable() {
2149 return false;
2150 }
2151
2152 /**
2153 * Subclasses override this to specify a default movement method.
2154 */
2155 protected MovementMethod getDefaultMovementMethod() {
2156 return null;
2157 }
2158
2159 /**
2160 * Return the text that TextView is displaying. If {@link #setText(CharSequence)} was called
2161 * with an argument of {@link android.widget.TextView.BufferType#SPANNABLE BufferType.SPANNABLE}
2162 * or {@link android.widget.TextView.BufferType#EDITABLE BufferType.EDITABLE}, you can cast
2163 * the return value from this method to Spannable or Editable, respectively.
2164 *
2165 * <p>The content of the return value should not be modified. If you want a modifiable one, you
2166 * should make your own copy first.</p>
2167 *
2168 * @return The text displayed by the text view.
2169 * @attr ref android.R.styleable#TextView_text
2170 */
2171 @ViewDebug.CapturedViewProperty
2172 public CharSequence getText() {
2173 return mText;
2174 }
2175
2176 /**
2177 * Returns the length, in characters, of the text managed by this TextView
2178 * @return The length of the text managed by the TextView in characters.
2179 */
2180 public int length() {
2181 return mText.length();
2182 }
2183
2184 /**
2185 * Return the text that TextView is displaying as an Editable object. If the text is not
2186 * editable, null is returned.
2187 *
2188 * @see #getText
2189 */
2190 public Editable getEditableText() {
2191 return (mText instanceof Editable) ? (Editable) mText : null;
2192 }
2193
2194 /**
2195 * Gets the vertical distance between lines of text, in pixels.
2196 * Note that markup within the text can cause individual lines
2197 * to be taller or shorter than this height, and the layout may
2198 * contain additional first-or last-line padding.
2199 * @return The height of one standard line in pixels.
2200 */
2201 public int getLineHeight() {
2202 return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
2203 }
2204
2205 /**
2206 * Gets the {@link android.text.Layout} that is currently being used to display the text.
2207 * This value can be null if the text or width has recently changed.
2208 * @return The Layout that is currently being used to display the text.
2209 */
2210 public final Layout getLayout() {
2211 return mLayout;
2212 }
2213
2214 /**
2215 * @return the {@link android.text.Layout} that is currently being used to
2216 * display the hint text. This can be null.
2217 */
2218 final Layout getHintLayout() {
2219 return mHintLayout;
2220 }
2221
2222 /**
2223 * Retrieve the {@link android.content.UndoManager} that is currently associated
2224 * with this TextView. By default there is no associated UndoManager, so null
2225 * is returned. One can be associated with the TextView through
2226 * {@link #setUndoManager(android.content.UndoManager, String)}
2227 *
2228 * @hide
2229 */
2230 public final UndoManager getUndoManager() {
2231 // TODO: Consider supporting a global undo manager.
2232 throw new UnsupportedOperationException("not implemented");
2233 }
2234
2235
2236 /**
2237 * @hide
2238 */
2239 @VisibleForTesting
2240 public final Editor getEditorForTesting() {
2241 return mEditor;
2242 }
2243
2244 /**
2245 * Associate an {@link android.content.UndoManager} with this TextView. Once
2246 * done, all edit operations on the TextView will result in appropriate
2247 * {@link android.content.UndoOperation} objects pushed on the given UndoManager's
2248 * stack.
2249 *
2250 * @param undoManager The {@link android.content.UndoManager} to associate with
2251 * this TextView, or null to clear any existing association.
2252 * @param tag String tag identifying this particular TextView owner in the
2253 * UndoManager. This is used to keep the correct association with the
2254 * {@link android.content.UndoOwner} of any operations inside of the UndoManager.
2255 *
2256 * @hide
2257 */
2258 public final void setUndoManager(UndoManager undoManager, String tag) {
2259 // TODO: Consider supporting a global undo manager. An implementation will need to:
2260 // * createEditorIfNeeded()
2261 // * Promote to BufferType.EDITABLE if needed.
2262 // * Update the UndoManager and UndoOwner.
2263 // Likewise it will need to be able to restore the default UndoManager.
2264 throw new UnsupportedOperationException("not implemented");
2265 }
2266
2267 /**
2268 * Gets the current {@link KeyListener} for the TextView.
2269 * This will frequently be null for non-EditText TextViews.
2270 * @return the current key listener for this TextView.
2271 *
2272 * @attr ref android.R.styleable#TextView_numeric
2273 * @attr ref android.R.styleable#TextView_digits
2274 * @attr ref android.R.styleable#TextView_phoneNumber
2275 * @attr ref android.R.styleable#TextView_inputMethod
2276 * @attr ref android.R.styleable#TextView_capitalize
2277 * @attr ref android.R.styleable#TextView_autoText
2278 */
2279 public final KeyListener getKeyListener() {
2280 return mEditor == null ? null : mEditor.mKeyListener;
2281 }
2282
2283 /**
2284 * Sets the key listener to be used with this TextView. This can be null
2285 * to disallow user input. Note that this method has significant and
2286 * subtle interactions with soft keyboards and other input method:
2287 * see {@link KeyListener#getInputType() KeyListener.getContentType()}
2288 * for important details. Calling this method will replace the current
2289 * content type of the text view with the content type returned by the
2290 * key listener.
2291 * <p>
2292 * Be warned that if you want a TextView with a key listener or movement
2293 * method not to be focusable, or if you want a TextView without a
2294 * key listener or movement method to be focusable, you must call
2295 * {@link #setFocusable} again after calling this to get the focusability
2296 * back the way you want it.
2297 *
2298 * @attr ref android.R.styleable#TextView_numeric
2299 * @attr ref android.R.styleable#TextView_digits
2300 * @attr ref android.R.styleable#TextView_phoneNumber
2301 * @attr ref android.R.styleable#TextView_inputMethod
2302 * @attr ref android.R.styleable#TextView_capitalize
2303 * @attr ref android.R.styleable#TextView_autoText
2304 */
2305 public void setKeyListener(KeyListener input) {
2306 mListenerChanged = true;
2307 setKeyListenerOnly(input);
2308 fixFocusableAndClickableSettings();
2309
2310 if (input != null) {
2311 createEditorIfNeeded();
2312 setInputTypeFromEditor();
2313 } else {
2314 if (mEditor != null) mEditor.mInputType = EditorInfo.TYPE_NULL;
2315 }
2316
2317 InputMethodManager imm = InputMethodManager.peekInstance();
2318 if (imm != null) imm.restartInput(this);
2319 }
2320
2321 private void setInputTypeFromEditor() {
2322 try {
2323 mEditor.mInputType = mEditor.mKeyListener.getInputType();
2324 } catch (IncompatibleClassChangeError e) {
2325 mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
2326 }
2327 // Change inputType, without affecting transformation.
2328 // No need to applySingleLine since mSingleLine is unchanged.
2329 setInputTypeSingleLine(mSingleLine);
2330 }
2331
2332 private void setKeyListenerOnly(KeyListener input) {
2333 if (mEditor == null && input == null) return; // null is the default value
2334
2335 createEditorIfNeeded();
2336 if (mEditor.mKeyListener != input) {
2337 mEditor.mKeyListener = input;
2338 if (input != null && !(mText instanceof Editable)) {
2339 setText(mText);
2340 }
2341
2342 setFilters((Editable) mText, mFilters);
2343 }
2344 }
2345
2346 /**
2347 * Gets the {@link android.text.method.MovementMethod} being used for this TextView,
2348 * which provides positioning, scrolling, and text selection functionality.
2349 * This will frequently be null for non-EditText TextViews.
2350 * @return the movement method being used for this TextView.
2351 * @see android.text.method.MovementMethod
2352 */
2353 public final MovementMethod getMovementMethod() {
2354 return mMovement;
2355 }
2356
2357 /**
2358 * Sets the {@link android.text.method.MovementMethod} for handling arrow key movement
2359 * for this TextView. This can be null to disallow using the arrow keys to move the
2360 * cursor or scroll the view.
2361 * <p>
2362 * Be warned that if you want a TextView with a key listener or movement
2363 * method not to be focusable, or if you want a TextView without a
2364 * key listener or movement method to be focusable, you must call
2365 * {@link #setFocusable} again after calling this to get the focusability
2366 * back the way you want it.
2367 */
2368 public final void setMovementMethod(MovementMethod movement) {
2369 if (mMovement != movement) {
2370 mMovement = movement;
2371
Justin Klaassen4d01eea2018-04-03 23:21:57 -04002372 if (movement != null && mSpannable == null) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04002373 setText(mText);
2374 }
2375
2376 fixFocusableAndClickableSettings();
2377
2378 // SelectionModifierCursorController depends on textCanBeSelected, which depends on
2379 // mMovement
2380 if (mEditor != null) mEditor.prepareCursorControllers();
2381 }
2382 }
2383
2384 private void fixFocusableAndClickableSettings() {
2385 if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {
2386 setFocusable(FOCUSABLE);
2387 setClickable(true);
2388 setLongClickable(true);
2389 } else {
2390 setFocusable(FOCUSABLE_AUTO);
2391 setClickable(false);
2392 setLongClickable(false);
2393 }
2394 }
2395
2396 /**
2397 * Gets the current {@link android.text.method.TransformationMethod} for the TextView.
2398 * This is frequently null, except for single-line and password fields.
2399 * @return the current transformation method for this TextView.
2400 *
2401 * @attr ref android.R.styleable#TextView_password
2402 * @attr ref android.R.styleable#TextView_singleLine
2403 */
2404 public final TransformationMethod getTransformationMethod() {
2405 return mTransformation;
2406 }
2407
2408 /**
2409 * Sets the transformation that is applied to the text that this
2410 * TextView is displaying.
2411 *
2412 * @attr ref android.R.styleable#TextView_password
2413 * @attr ref android.R.styleable#TextView_singleLine
2414 */
2415 public final void setTransformationMethod(TransformationMethod method) {
2416 if (method == mTransformation) {
2417 // Avoid the setText() below if the transformation is
2418 // the same.
2419 return;
2420 }
2421 if (mTransformation != null) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04002422 if (mSpannable != null) {
2423 mSpannable.removeSpan(mTransformation);
Justin Klaassen10d07c82017-09-15 17:58:39 -04002424 }
2425 }
2426
2427 mTransformation = method;
2428
2429 if (method instanceof TransformationMethod2) {
2430 TransformationMethod2 method2 = (TransformationMethod2) method;
2431 mAllowTransformationLengthChange = !isTextSelectable() && !(mText instanceof Editable);
2432 method2.setLengthChangesAllowed(mAllowTransformationLengthChange);
2433 } else {
2434 mAllowTransformationLengthChange = false;
2435 }
2436
2437 setText(mText);
2438
2439 if (hasPasswordTransformationMethod()) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04002440 notifyViewAccessibilityStateChangedIfNeeded(
Justin Klaassen10d07c82017-09-15 17:58:39 -04002441 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
2442 }
2443
2444 // PasswordTransformationMethod always have LTR text direction heuristics returned by
2445 // getTextDirectionHeuristic, needs reset
2446 mTextDir = getTextDirectionHeuristic();
2447 }
2448
2449 /**
2450 * Returns the top padding of the view, plus space for the top
2451 * Drawable if any.
2452 */
2453 public int getCompoundPaddingTop() {
2454 final Drawables dr = mDrawables;
2455 if (dr == null || dr.mShowing[Drawables.TOP] == null) {
2456 return mPaddingTop;
2457 } else {
2458 return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
2459 }
2460 }
2461
2462 /**
2463 * Returns the bottom padding of the view, plus space for the bottom
2464 * Drawable if any.
2465 */
2466 public int getCompoundPaddingBottom() {
2467 final Drawables dr = mDrawables;
2468 if (dr == null || dr.mShowing[Drawables.BOTTOM] == null) {
2469 return mPaddingBottom;
2470 } else {
2471 return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
2472 }
2473 }
2474
2475 /**
2476 * Returns the left padding of the view, plus space for the left
2477 * Drawable if any.
2478 */
2479 public int getCompoundPaddingLeft() {
2480 final Drawables dr = mDrawables;
2481 if (dr == null || dr.mShowing[Drawables.LEFT] == null) {
2482 return mPaddingLeft;
2483 } else {
2484 return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
2485 }
2486 }
2487
2488 /**
2489 * Returns the right padding of the view, plus space for the right
2490 * Drawable if any.
2491 */
2492 public int getCompoundPaddingRight() {
2493 final Drawables dr = mDrawables;
2494 if (dr == null || dr.mShowing[Drawables.RIGHT] == null) {
2495 return mPaddingRight;
2496 } else {
2497 return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
2498 }
2499 }
2500
2501 /**
2502 * Returns the start padding of the view, plus space for the start
2503 * Drawable if any.
2504 */
2505 public int getCompoundPaddingStart() {
2506 resolveDrawables();
2507 switch(getLayoutDirection()) {
2508 default:
2509 case LAYOUT_DIRECTION_LTR:
2510 return getCompoundPaddingLeft();
2511 case LAYOUT_DIRECTION_RTL:
2512 return getCompoundPaddingRight();
2513 }
2514 }
2515
2516 /**
2517 * Returns the end padding of the view, plus space for the end
2518 * Drawable if any.
2519 */
2520 public int getCompoundPaddingEnd() {
2521 resolveDrawables();
2522 switch(getLayoutDirection()) {
2523 default:
2524 case LAYOUT_DIRECTION_LTR:
2525 return getCompoundPaddingRight();
2526 case LAYOUT_DIRECTION_RTL:
2527 return getCompoundPaddingLeft();
2528 }
2529 }
2530
2531 /**
2532 * Returns the extended top padding of the view, including both the
2533 * top Drawable if any and any extra space to keep more than maxLines
2534 * of text from showing. It is only valid to call this after measuring.
2535 */
2536 public int getExtendedPaddingTop() {
2537 if (mMaxMode != LINES) {
2538 return getCompoundPaddingTop();
2539 }
2540
2541 if (mLayout == null) {
2542 assumeLayout();
2543 }
2544
2545 if (mLayout.getLineCount() <= mMaximum) {
2546 return getCompoundPaddingTop();
2547 }
2548
2549 int top = getCompoundPaddingTop();
2550 int bottom = getCompoundPaddingBottom();
2551 int viewht = getHeight() - top - bottom;
2552 int layoutht = mLayout.getLineTop(mMaximum);
2553
2554 if (layoutht >= viewht) {
2555 return top;
2556 }
2557
2558 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
2559 if (gravity == Gravity.TOP) {
2560 return top;
2561 } else if (gravity == Gravity.BOTTOM) {
2562 return top + viewht - layoutht;
2563 } else { // (gravity == Gravity.CENTER_VERTICAL)
2564 return top + (viewht - layoutht) / 2;
2565 }
2566 }
2567
2568 /**
2569 * Returns the extended bottom padding of the view, including both the
2570 * bottom Drawable if any and any extra space to keep more than maxLines
2571 * of text from showing. It is only valid to call this after measuring.
2572 */
2573 public int getExtendedPaddingBottom() {
2574 if (mMaxMode != LINES) {
2575 return getCompoundPaddingBottom();
2576 }
2577
2578 if (mLayout == null) {
2579 assumeLayout();
2580 }
2581
2582 if (mLayout.getLineCount() <= mMaximum) {
2583 return getCompoundPaddingBottom();
2584 }
2585
2586 int top = getCompoundPaddingTop();
2587 int bottom = getCompoundPaddingBottom();
2588 int viewht = getHeight() - top - bottom;
2589 int layoutht = mLayout.getLineTop(mMaximum);
2590
2591 if (layoutht >= viewht) {
2592 return bottom;
2593 }
2594
2595 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
2596 if (gravity == Gravity.TOP) {
2597 return bottom + viewht - layoutht;
2598 } else if (gravity == Gravity.BOTTOM) {
2599 return bottom;
2600 } else { // (gravity == Gravity.CENTER_VERTICAL)
2601 return bottom + (viewht - layoutht) / 2;
2602 }
2603 }
2604
2605 /**
2606 * Returns the total left padding of the view, including the left
2607 * Drawable if any.
2608 */
2609 public int getTotalPaddingLeft() {
2610 return getCompoundPaddingLeft();
2611 }
2612
2613 /**
2614 * Returns the total right padding of the view, including the right
2615 * Drawable if any.
2616 */
2617 public int getTotalPaddingRight() {
2618 return getCompoundPaddingRight();
2619 }
2620
2621 /**
2622 * Returns the total start padding of the view, including the start
2623 * Drawable if any.
2624 */
2625 public int getTotalPaddingStart() {
2626 return getCompoundPaddingStart();
2627 }
2628
2629 /**
2630 * Returns the total end padding of the view, including the end
2631 * Drawable if any.
2632 */
2633 public int getTotalPaddingEnd() {
2634 return getCompoundPaddingEnd();
2635 }
2636
2637 /**
2638 * Returns the total top padding of the view, including the top
2639 * Drawable if any, the extra space to keep more than maxLines
2640 * from showing, and the vertical offset for gravity, if any.
2641 */
2642 public int getTotalPaddingTop() {
2643 return getExtendedPaddingTop() + getVerticalOffset(true);
2644 }
2645
2646 /**
2647 * Returns the total bottom padding of the view, including the bottom
2648 * Drawable if any, the extra space to keep more than maxLines
2649 * from showing, and the vertical offset for gravity, if any.
2650 */
2651 public int getTotalPaddingBottom() {
2652 return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
2653 }
2654
2655 /**
2656 * Sets the Drawables (if any) to appear to the left of, above, to the
2657 * right of, and below the text. Use {@code null} if you do not want a
2658 * Drawable there. The Drawables must already have had
2659 * {@link Drawable#setBounds} called.
2660 * <p>
2661 * Calling this method will overwrite any Drawables previously set using
2662 * {@link #setCompoundDrawablesRelative} or related methods.
2663 *
2664 * @attr ref android.R.styleable#TextView_drawableLeft
2665 * @attr ref android.R.styleable#TextView_drawableTop
2666 * @attr ref android.R.styleable#TextView_drawableRight
2667 * @attr ref android.R.styleable#TextView_drawableBottom
2668 */
2669 public void setCompoundDrawables(@Nullable Drawable left, @Nullable Drawable top,
2670 @Nullable Drawable right, @Nullable Drawable bottom) {
2671 Drawables dr = mDrawables;
2672
2673 // We're switching to absolute, discard relative.
2674 if (dr != null) {
2675 if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
2676 dr.mDrawableStart = null;
2677 if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null);
2678 dr.mDrawableEnd = null;
2679 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
2680 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
2681 }
2682
2683 final boolean drawables = left != null || top != null || right != null || bottom != null;
2684 if (!drawables) {
2685 // Clearing drawables... can we free the data structure?
2686 if (dr != null) {
2687 if (!dr.hasMetadata()) {
2688 mDrawables = null;
2689 } else {
2690 // We need to retain the last set padding, so just clear
2691 // out all of the fields in the existing structure.
2692 for (int i = dr.mShowing.length - 1; i >= 0; i--) {
2693 if (dr.mShowing[i] != null) {
2694 dr.mShowing[i].setCallback(null);
2695 }
2696 dr.mShowing[i] = null;
2697 }
2698 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
2699 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
2700 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
2701 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
2702 }
2703 }
2704 } else {
2705 if (dr == null) {
2706 mDrawables = dr = new Drawables(getContext());
2707 }
2708
2709 mDrawables.mOverride = false;
2710
2711 if (dr.mShowing[Drawables.LEFT] != left && dr.mShowing[Drawables.LEFT] != null) {
2712 dr.mShowing[Drawables.LEFT].setCallback(null);
2713 }
2714 dr.mShowing[Drawables.LEFT] = left;
2715
2716 if (dr.mShowing[Drawables.TOP] != top && dr.mShowing[Drawables.TOP] != null) {
2717 dr.mShowing[Drawables.TOP].setCallback(null);
2718 }
2719 dr.mShowing[Drawables.TOP] = top;
2720
2721 if (dr.mShowing[Drawables.RIGHT] != right && dr.mShowing[Drawables.RIGHT] != null) {
2722 dr.mShowing[Drawables.RIGHT].setCallback(null);
2723 }
2724 dr.mShowing[Drawables.RIGHT] = right;
2725
2726 if (dr.mShowing[Drawables.BOTTOM] != bottom && dr.mShowing[Drawables.BOTTOM] != null) {
2727 dr.mShowing[Drawables.BOTTOM].setCallback(null);
2728 }
2729 dr.mShowing[Drawables.BOTTOM] = bottom;
2730
2731 final Rect compoundRect = dr.mCompoundRect;
2732 int[] state;
2733
2734 state = getDrawableState();
2735
2736 if (left != null) {
2737 left.setState(state);
2738 left.copyBounds(compoundRect);
2739 left.setCallback(this);
2740 dr.mDrawableSizeLeft = compoundRect.width();
2741 dr.mDrawableHeightLeft = compoundRect.height();
2742 } else {
2743 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
2744 }
2745
2746 if (right != null) {
2747 right.setState(state);
2748 right.copyBounds(compoundRect);
2749 right.setCallback(this);
2750 dr.mDrawableSizeRight = compoundRect.width();
2751 dr.mDrawableHeightRight = compoundRect.height();
2752 } else {
2753 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
2754 }
2755
2756 if (top != null) {
2757 top.setState(state);
2758 top.copyBounds(compoundRect);
2759 top.setCallback(this);
2760 dr.mDrawableSizeTop = compoundRect.height();
2761 dr.mDrawableWidthTop = compoundRect.width();
2762 } else {
2763 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
2764 }
2765
2766 if (bottom != null) {
2767 bottom.setState(state);
2768 bottom.copyBounds(compoundRect);
2769 bottom.setCallback(this);
2770 dr.mDrawableSizeBottom = compoundRect.height();
2771 dr.mDrawableWidthBottom = compoundRect.width();
2772 } else {
2773 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
2774 }
2775 }
2776
2777 // Save initial left/right drawables
2778 if (dr != null) {
2779 dr.mDrawableLeftInitial = left;
2780 dr.mDrawableRightInitial = right;
2781 }
2782
2783 resetResolvedDrawables();
2784 resolveDrawables();
2785 applyCompoundDrawableTint();
2786 invalidate();
2787 requestLayout();
2788 }
2789
2790 /**
2791 * Sets the Drawables (if any) to appear to the left of, above, to the
2792 * right of, and below the text. Use 0 if you do not want a Drawable there.
2793 * The Drawables' bounds will be set to their intrinsic bounds.
2794 * <p>
2795 * Calling this method will overwrite any Drawables previously set using
2796 * {@link #setCompoundDrawablesRelative} or related methods.
2797 *
2798 * @param left Resource identifier of the left Drawable.
2799 * @param top Resource identifier of the top Drawable.
2800 * @param right Resource identifier of the right Drawable.
2801 * @param bottom Resource identifier of the bottom Drawable.
2802 *
2803 * @attr ref android.R.styleable#TextView_drawableLeft
2804 * @attr ref android.R.styleable#TextView_drawableTop
2805 * @attr ref android.R.styleable#TextView_drawableRight
2806 * @attr ref android.R.styleable#TextView_drawableBottom
2807 */
2808 @android.view.RemotableViewMethod
2809 public void setCompoundDrawablesWithIntrinsicBounds(@DrawableRes int left,
2810 @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
2811 final Context context = getContext();
2812 setCompoundDrawablesWithIntrinsicBounds(left != 0 ? context.getDrawable(left) : null,
2813 top != 0 ? context.getDrawable(top) : null,
2814 right != 0 ? context.getDrawable(right) : null,
2815 bottom != 0 ? context.getDrawable(bottom) : null);
2816 }
2817
2818 /**
2819 * Sets the Drawables (if any) to appear to the left of, above, to the
2820 * right of, and below the text. Use {@code null} if you do not want a
2821 * Drawable there. The Drawables' bounds will be set to their intrinsic
2822 * bounds.
2823 * <p>
2824 * Calling this method will overwrite any Drawables previously set using
2825 * {@link #setCompoundDrawablesRelative} or related methods.
2826 *
2827 * @attr ref android.R.styleable#TextView_drawableLeft
2828 * @attr ref android.R.styleable#TextView_drawableTop
2829 * @attr ref android.R.styleable#TextView_drawableRight
2830 * @attr ref android.R.styleable#TextView_drawableBottom
2831 */
2832 @android.view.RemotableViewMethod
2833 public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left,
2834 @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom) {
2835
2836 if (left != null) {
2837 left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
2838 }
2839 if (right != null) {
2840 right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
2841 }
2842 if (top != null) {
2843 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
2844 }
2845 if (bottom != null) {
2846 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
2847 }
2848 setCompoundDrawables(left, top, right, bottom);
2849 }
2850
2851 /**
2852 * Sets the Drawables (if any) to appear to the start of, above, to the end
2853 * of, and below the text. Use {@code null} if you do not want a Drawable
2854 * there. The Drawables must already have had {@link Drawable#setBounds}
2855 * called.
2856 * <p>
2857 * Calling this method will overwrite any Drawables previously set using
2858 * {@link #setCompoundDrawables} or related methods.
2859 *
2860 * @attr ref android.R.styleable#TextView_drawableStart
2861 * @attr ref android.R.styleable#TextView_drawableTop
2862 * @attr ref android.R.styleable#TextView_drawableEnd
2863 * @attr ref android.R.styleable#TextView_drawableBottom
2864 */
2865 @android.view.RemotableViewMethod
2866 public void setCompoundDrawablesRelative(@Nullable Drawable start, @Nullable Drawable top,
2867 @Nullable Drawable end, @Nullable Drawable bottom) {
2868 Drawables dr = mDrawables;
2869
2870 // We're switching to relative, discard absolute.
2871 if (dr != null) {
2872 if (dr.mShowing[Drawables.LEFT] != null) {
2873 dr.mShowing[Drawables.LEFT].setCallback(null);
2874 }
2875 dr.mShowing[Drawables.LEFT] = dr.mDrawableLeftInitial = null;
2876 if (dr.mShowing[Drawables.RIGHT] != null) {
2877 dr.mShowing[Drawables.RIGHT].setCallback(null);
2878 }
2879 dr.mShowing[Drawables.RIGHT] = dr.mDrawableRightInitial = null;
2880 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
2881 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
2882 }
2883
2884 final boolean drawables = start != null || top != null
2885 || end != null || bottom != null;
2886
2887 if (!drawables) {
2888 // Clearing drawables... can we free the data structure?
2889 if (dr != null) {
2890 if (!dr.hasMetadata()) {
2891 mDrawables = null;
2892 } else {
2893 // We need to retain the last set padding, so just clear
2894 // out all of the fields in the existing structure.
2895 if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
2896 dr.mDrawableStart = null;
2897 if (dr.mShowing[Drawables.TOP] != null) {
2898 dr.mShowing[Drawables.TOP].setCallback(null);
2899 }
2900 dr.mShowing[Drawables.TOP] = null;
2901 if (dr.mDrawableEnd != null) {
2902 dr.mDrawableEnd.setCallback(null);
2903 }
2904 dr.mDrawableEnd = null;
2905 if (dr.mShowing[Drawables.BOTTOM] != null) {
2906 dr.mShowing[Drawables.BOTTOM].setCallback(null);
2907 }
2908 dr.mShowing[Drawables.BOTTOM] = null;
2909 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
2910 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
2911 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
2912 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
2913 }
2914 }
2915 } else {
2916 if (dr == null) {
2917 mDrawables = dr = new Drawables(getContext());
2918 }
2919
2920 mDrawables.mOverride = true;
2921
2922 if (dr.mDrawableStart != start && dr.mDrawableStart != null) {
2923 dr.mDrawableStart.setCallback(null);
2924 }
2925 dr.mDrawableStart = start;
2926
2927 if (dr.mShowing[Drawables.TOP] != top && dr.mShowing[Drawables.TOP] != null) {
2928 dr.mShowing[Drawables.TOP].setCallback(null);
2929 }
2930 dr.mShowing[Drawables.TOP] = top;
2931
2932 if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) {
2933 dr.mDrawableEnd.setCallback(null);
2934 }
2935 dr.mDrawableEnd = end;
2936
2937 if (dr.mShowing[Drawables.BOTTOM] != bottom && dr.mShowing[Drawables.BOTTOM] != null) {
2938 dr.mShowing[Drawables.BOTTOM].setCallback(null);
2939 }
2940 dr.mShowing[Drawables.BOTTOM] = bottom;
2941
2942 final Rect compoundRect = dr.mCompoundRect;
2943 int[] state;
2944
2945 state = getDrawableState();
2946
2947 if (start != null) {
2948 start.setState(state);
2949 start.copyBounds(compoundRect);
2950 start.setCallback(this);
2951 dr.mDrawableSizeStart = compoundRect.width();
2952 dr.mDrawableHeightStart = compoundRect.height();
2953 } else {
2954 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
2955 }
2956
2957 if (end != null) {
2958 end.setState(state);
2959 end.copyBounds(compoundRect);
2960 end.setCallback(this);
2961 dr.mDrawableSizeEnd = compoundRect.width();
2962 dr.mDrawableHeightEnd = compoundRect.height();
2963 } else {
2964 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
2965 }
2966
2967 if (top != null) {
2968 top.setState(state);
2969 top.copyBounds(compoundRect);
2970 top.setCallback(this);
2971 dr.mDrawableSizeTop = compoundRect.height();
2972 dr.mDrawableWidthTop = compoundRect.width();
2973 } else {
2974 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
2975 }
2976
2977 if (bottom != null) {
2978 bottom.setState(state);
2979 bottom.copyBounds(compoundRect);
2980 bottom.setCallback(this);
2981 dr.mDrawableSizeBottom = compoundRect.height();
2982 dr.mDrawableWidthBottom = compoundRect.width();
2983 } else {
2984 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
2985 }
2986 }
2987
2988 resetResolvedDrawables();
2989 resolveDrawables();
2990 invalidate();
2991 requestLayout();
2992 }
2993
2994 /**
2995 * Sets the Drawables (if any) to appear to the start of, above, to the end
2996 * of, and below the text. Use 0 if you do not want a Drawable there. The
2997 * Drawables' bounds will be set to their intrinsic bounds.
2998 * <p>
2999 * Calling this method will overwrite any Drawables previously set using
3000 * {@link #setCompoundDrawables} or related methods.
3001 *
3002 * @param start Resource identifier of the start Drawable.
3003 * @param top Resource identifier of the top Drawable.
3004 * @param end Resource identifier of the end Drawable.
3005 * @param bottom Resource identifier of the bottom Drawable.
3006 *
3007 * @attr ref android.R.styleable#TextView_drawableStart
3008 * @attr ref android.R.styleable#TextView_drawableTop
3009 * @attr ref android.R.styleable#TextView_drawableEnd
3010 * @attr ref android.R.styleable#TextView_drawableBottom
3011 */
3012 @android.view.RemotableViewMethod
3013 public void setCompoundDrawablesRelativeWithIntrinsicBounds(@DrawableRes int start,
3014 @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
3015 final Context context = getContext();
3016 setCompoundDrawablesRelativeWithIntrinsicBounds(
3017 start != 0 ? context.getDrawable(start) : null,
3018 top != 0 ? context.getDrawable(top) : null,
3019 end != 0 ? context.getDrawable(end) : null,
3020 bottom != 0 ? context.getDrawable(bottom) : null);
3021 }
3022
3023 /**
3024 * Sets the Drawables (if any) to appear to the start of, above, to the end
3025 * of, and below the text. Use {@code null} if you do not want a Drawable
3026 * there. The Drawables' bounds will be set to their intrinsic bounds.
3027 * <p>
3028 * Calling this method will overwrite any Drawables previously set using
3029 * {@link #setCompoundDrawables} or related methods.
3030 *
3031 * @attr ref android.R.styleable#TextView_drawableStart
3032 * @attr ref android.R.styleable#TextView_drawableTop
3033 * @attr ref android.R.styleable#TextView_drawableEnd
3034 * @attr ref android.R.styleable#TextView_drawableBottom
3035 */
3036 @android.view.RemotableViewMethod
3037 public void setCompoundDrawablesRelativeWithIntrinsicBounds(@Nullable Drawable start,
3038 @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom) {
3039
3040 if (start != null) {
3041 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
3042 }
3043 if (end != null) {
3044 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
3045 }
3046 if (top != null) {
3047 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
3048 }
3049 if (bottom != null) {
3050 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
3051 }
3052 setCompoundDrawablesRelative(start, top, end, bottom);
3053 }
3054
3055 /**
3056 * Returns drawables for the left, top, right, and bottom borders.
3057 *
3058 * @attr ref android.R.styleable#TextView_drawableLeft
3059 * @attr ref android.R.styleable#TextView_drawableTop
3060 * @attr ref android.R.styleable#TextView_drawableRight
3061 * @attr ref android.R.styleable#TextView_drawableBottom
3062 */
3063 @NonNull
3064 public Drawable[] getCompoundDrawables() {
3065 final Drawables dr = mDrawables;
3066 if (dr != null) {
3067 return dr.mShowing.clone();
3068 } else {
3069 return new Drawable[] { null, null, null, null };
3070 }
3071 }
3072
3073 /**
3074 * Returns drawables for the start, top, end, and bottom borders.
3075 *
3076 * @attr ref android.R.styleable#TextView_drawableStart
3077 * @attr ref android.R.styleable#TextView_drawableTop
3078 * @attr ref android.R.styleable#TextView_drawableEnd
3079 * @attr ref android.R.styleable#TextView_drawableBottom
3080 */
3081 @NonNull
3082 public Drawable[] getCompoundDrawablesRelative() {
3083 final Drawables dr = mDrawables;
3084 if (dr != null) {
3085 return new Drawable[] {
3086 dr.mDrawableStart, dr.mShowing[Drawables.TOP],
3087 dr.mDrawableEnd, dr.mShowing[Drawables.BOTTOM]
3088 };
3089 } else {
3090 return new Drawable[] { null, null, null, null };
3091 }
3092 }
3093
3094 /**
3095 * Sets the size of the padding between the compound drawables and
3096 * the text.
3097 *
3098 * @attr ref android.R.styleable#TextView_drawablePadding
3099 */
3100 @android.view.RemotableViewMethod
3101 public void setCompoundDrawablePadding(int pad) {
3102 Drawables dr = mDrawables;
3103 if (pad == 0) {
3104 if (dr != null) {
3105 dr.mDrawablePadding = pad;
3106 }
3107 } else {
3108 if (dr == null) {
3109 mDrawables = dr = new Drawables(getContext());
3110 }
3111 dr.mDrawablePadding = pad;
3112 }
3113
3114 invalidate();
3115 requestLayout();
3116 }
3117
3118 /**
3119 * Returns the padding between the compound drawables and the text.
3120 *
3121 * @attr ref android.R.styleable#TextView_drawablePadding
3122 */
3123 public int getCompoundDrawablePadding() {
3124 final Drawables dr = mDrawables;
3125 return dr != null ? dr.mDrawablePadding : 0;
3126 }
3127
3128 /**
3129 * Applies a tint to the compound drawables. Does not modify the
3130 * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
3131 * <p>
3132 * Subsequent calls to
3133 * {@link #setCompoundDrawables(Drawable, Drawable, Drawable, Drawable)}
3134 * and related methods will automatically mutate the drawables and apply
3135 * the specified tint and tint mode using
3136 * {@link Drawable#setTintList(ColorStateList)}.
3137 *
3138 * @param tint the tint to apply, may be {@code null} to clear tint
3139 *
3140 * @attr ref android.R.styleable#TextView_drawableTint
3141 * @see #getCompoundDrawableTintList()
3142 * @see Drawable#setTintList(ColorStateList)
3143 */
3144 public void setCompoundDrawableTintList(@Nullable ColorStateList tint) {
3145 if (mDrawables == null) {
3146 mDrawables = new Drawables(getContext());
3147 }
3148 mDrawables.mTintList = tint;
3149 mDrawables.mHasTint = true;
3150
3151 applyCompoundDrawableTint();
3152 }
3153
3154 /**
3155 * @return the tint applied to the compound drawables
3156 * @attr ref android.R.styleable#TextView_drawableTint
3157 * @see #setCompoundDrawableTintList(ColorStateList)
3158 */
3159 public ColorStateList getCompoundDrawableTintList() {
3160 return mDrawables != null ? mDrawables.mTintList : null;
3161 }
3162
3163 /**
3164 * Specifies the blending mode used to apply the tint specified by
3165 * {@link #setCompoundDrawableTintList(ColorStateList)} to the compound
3166 * drawables. The default mode is {@link PorterDuff.Mode#SRC_IN}.
3167 *
3168 * @param tintMode the blending mode used to apply the tint, may be
3169 * {@code null} to clear tint
3170 * @attr ref android.R.styleable#TextView_drawableTintMode
3171 * @see #setCompoundDrawableTintList(ColorStateList)
3172 * @see Drawable#setTintMode(PorterDuff.Mode)
3173 */
3174 public void setCompoundDrawableTintMode(@Nullable PorterDuff.Mode tintMode) {
3175 if (mDrawables == null) {
3176 mDrawables = new Drawables(getContext());
3177 }
3178 mDrawables.mTintMode = tintMode;
3179 mDrawables.mHasTintMode = true;
3180
3181 applyCompoundDrawableTint();
3182 }
3183
3184 /**
3185 * Returns the blending mode used to apply the tint to the compound
3186 * drawables, if specified.
3187 *
3188 * @return the blending mode used to apply the tint to the compound
3189 * drawables
3190 * @attr ref android.R.styleable#TextView_drawableTintMode
3191 * @see #setCompoundDrawableTintMode(PorterDuff.Mode)
3192 */
3193 public PorterDuff.Mode getCompoundDrawableTintMode() {
3194 return mDrawables != null ? mDrawables.mTintMode : null;
3195 }
3196
3197 private void applyCompoundDrawableTint() {
3198 if (mDrawables == null) {
3199 return;
3200 }
3201
3202 if (mDrawables.mHasTint || mDrawables.mHasTintMode) {
3203 final ColorStateList tintList = mDrawables.mTintList;
3204 final PorterDuff.Mode tintMode = mDrawables.mTintMode;
3205 final boolean hasTint = mDrawables.mHasTint;
3206 final boolean hasTintMode = mDrawables.mHasTintMode;
3207 final int[] state = getDrawableState();
3208
3209 for (Drawable dr : mDrawables.mShowing) {
3210 if (dr == null) {
3211 continue;
3212 }
3213
3214 if (dr == mDrawables.mDrawableError) {
3215 // From a developer's perspective, the error drawable isn't
3216 // a compound drawable. Don't apply the generic compound
3217 // drawable tint to it.
3218 continue;
3219 }
3220
3221 dr.mutate();
3222
3223 if (hasTint) {
3224 dr.setTintList(tintList);
3225 }
3226
3227 if (hasTintMode) {
3228 dr.setTintMode(tintMode);
3229 }
3230
3231 // The drawable (or one of its children) may not have been
3232 // stateful before applying the tint, so let's try again.
3233 if (dr.isStateful()) {
3234 dr.setState(state);
3235 }
3236 }
3237 }
3238 }
3239
Jeff Davidsona192cc22018-02-08 15:30:06 -08003240 /**
3241 * @inheritDoc
3242 *
3243 * @see #setFirstBaselineToTopHeight(int)
3244 * @see #setLastBaselineToBottomHeight(int)
3245 */
Justin Klaassen10d07c82017-09-15 17:58:39 -04003246 @Override
3247 public void setPadding(int left, int top, int right, int bottom) {
3248 if (left != mPaddingLeft
3249 || right != mPaddingRight
3250 || top != mPaddingTop
3251 || bottom != mPaddingBottom) {
3252 nullLayouts();
3253 }
3254
3255 // the super call will requestLayout()
3256 super.setPadding(left, top, right, bottom);
3257 invalidate();
3258 }
3259
Jeff Davidsona192cc22018-02-08 15:30:06 -08003260 /**
3261 * @inheritDoc
3262 *
3263 * @see #setFirstBaselineToTopHeight(int)
3264 * @see #setLastBaselineToBottomHeight(int)
3265 */
Justin Klaassen10d07c82017-09-15 17:58:39 -04003266 @Override
3267 public void setPaddingRelative(int start, int top, int end, int bottom) {
3268 if (start != getPaddingStart()
3269 || end != getPaddingEnd()
3270 || top != mPaddingTop
3271 || bottom != mPaddingBottom) {
3272 nullLayouts();
3273 }
3274
3275 // the super call will requestLayout()
3276 super.setPaddingRelative(start, top, end, bottom);
3277 invalidate();
3278 }
3279
3280 /**
Jeff Davidsona192cc22018-02-08 15:30:06 -08003281 * Updates the top padding of the TextView so that {@code firstBaselineToTopHeight} is
3282 * equal to the distance between the firt text baseline and the top of this TextView.
3283 * <strong>Note</strong> that if {@code FontMetrics.top} or {@code FontMetrics.ascent} was
3284 * already greater than {@code firstBaselineToTopHeight}, the top padding is not updated.
3285 *
3286 * @param firstBaselineToTopHeight distance between first baseline to top of the container
3287 * in pixels
3288 *
3289 * @see #getFirstBaselineToTopHeight()
3290 * @see #setPadding(int, int, int, int)
3291 * @see #setPaddingRelative(int, int, int, int)
3292 *
3293 * @attr ref android.R.styleable#TextView_firstBaselineToTopHeight
3294 */
3295 public void setFirstBaselineToTopHeight(@Px @IntRange(from = 0) int firstBaselineToTopHeight) {
3296 Preconditions.checkArgumentNonnegative(firstBaselineToTopHeight);
3297
3298 final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
3299 final int fontMetricsTop;
3300 if (getIncludeFontPadding()) {
3301 fontMetricsTop = fontMetrics.top;
3302 } else {
3303 fontMetricsTop = fontMetrics.ascent;
3304 }
3305
3306 // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
3307 // in settings). At the moment, we don't.
3308
3309 if (firstBaselineToTopHeight > Math.abs(fontMetricsTop)) {
3310 final int paddingTop = firstBaselineToTopHeight - (-fontMetricsTop);
3311 setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), getPaddingBottom());
3312 }
3313 }
3314
3315 /**
3316 * Updates the bottom padding of the TextView so that {@code lastBaselineToBottomHeight} is
3317 * equal to the distance between the last text baseline and the bottom of this TextView.
3318 * <strong>Note</strong> that if {@code FontMetrics.bottom} or {@code FontMetrics.descent} was
3319 * already greater than {@code lastBaselineToBottomHeight}, the bottom padding is not updated.
3320 *
3321 * @param lastBaselineToBottomHeight distance between last baseline to bottom of the container
3322 * in pixels
3323 *
3324 * @see #getLastBaselineToBottomHeight()
3325 * @see #setPadding(int, int, int, int)
3326 * @see #setPaddingRelative(int, int, int, int)
3327 *
3328 * @attr ref android.R.styleable#TextView_lastBaselineToBottomHeight
3329 */
3330 public void setLastBaselineToBottomHeight(
3331 @Px @IntRange(from = 0) int lastBaselineToBottomHeight) {
3332 Preconditions.checkArgumentNonnegative(lastBaselineToBottomHeight);
3333
3334 final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
3335 final int fontMetricsBottom;
3336 if (getIncludeFontPadding()) {
3337 fontMetricsBottom = fontMetrics.bottom;
3338 } else {
3339 fontMetricsBottom = fontMetrics.descent;
3340 }
3341
3342 // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
3343 // in settings). At the moment, we don't.
3344
3345 if (lastBaselineToBottomHeight > Math.abs(fontMetricsBottom)) {
3346 final int paddingBottom = lastBaselineToBottomHeight - fontMetricsBottom;
3347 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), paddingBottom);
3348 }
3349 }
3350
3351 /**
3352 * Returns the distance between the first text baseline and the top of this TextView.
3353 *
3354 * @see #setFirstBaselineToTopHeight(int)
3355 * @attr ref android.R.styleable#TextView_firstBaselineToTopHeight
3356 */
3357 public int getFirstBaselineToTopHeight() {
3358 return getPaddingTop() - getPaint().getFontMetricsInt().top;
3359 }
3360
3361 /**
3362 * Returns the distance between the last text baseline and the bottom of this TextView.
3363 *
3364 * @see #setLastBaselineToBottomHeight(int)
3365 * @attr ref android.R.styleable#TextView_lastBaselineToBottomHeight
3366 */
3367 public int getLastBaselineToBottomHeight() {
3368 return getPaddingBottom() + getPaint().getFontMetricsInt().bottom;
3369 }
3370
3371 /**
Justin Klaassen10d07c82017-09-15 17:58:39 -04003372 * Gets the autolink mask of the text. See {@link
3373 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
3374 * possible values.
3375 *
3376 * @attr ref android.R.styleable#TextView_autoLink
3377 */
3378 public final int getAutoLinkMask() {
3379 return mAutoLinkMask;
3380 }
3381
3382 /**
3383 * Sets the text appearance from the specified style resource.
3384 * <p>
3385 * Use a framework-defined {@code TextAppearance} style like
3386 * {@link android.R.style#TextAppearance_Material_Body1 @android:style/TextAppearance.Material.Body1}
3387 * or see {@link android.R.styleable#TextAppearance TextAppearance} for the
3388 * set of attributes that can be used in a custom style.
3389 *
3390 * @param resId the resource identifier of the style to apply
3391 * @attr ref android.R.styleable#TextView_textAppearance
3392 */
3393 @SuppressWarnings("deprecation")
3394 public void setTextAppearance(@StyleRes int resId) {
3395 setTextAppearance(mContext, resId);
3396 }
3397
3398 /**
3399 * Sets the text color, size, style, hint color, and highlight color
3400 * from the specified TextAppearance resource.
3401 *
3402 * @deprecated Use {@link #setTextAppearance(int)} instead.
3403 */
3404 @Deprecated
3405 public void setTextAppearance(Context context, @StyleRes int resId) {
3406 final TypedArray ta = context.obtainStyledAttributes(resId, R.styleable.TextAppearance);
3407 final TextAppearanceAttributes attributes = new TextAppearanceAttributes();
3408 readTextAppearance(context, ta, attributes, false /* styleArray */);
3409 ta.recycle();
3410 applyTextAppearance(attributes);
3411 }
3412
3413 /**
3414 * Set of attributes that can be defined in a Text Appearance. This is used to simplify the code
3415 * that reads these attributes in the constructor and in {@link #setTextAppearance}.
3416 */
3417 private static class TextAppearanceAttributes {
3418 int mTextColorHighlight = 0;
3419 ColorStateList mTextColor = null;
3420 ColorStateList mTextColorHint = null;
3421 ColorStateList mTextColorLink = null;
3422 int mTextSize = 0;
3423 String mFontFamily = null;
3424 Typeface mFontTypeface = null;
3425 boolean mFontFamilyExplicit = false;
3426 int mTypefaceIndex = -1;
3427 int mStyleIndex = -1;
Justin Klaassen4d01eea2018-04-03 23:21:57 -04003428 int mFontWeight = -1;
Justin Klaassen10d07c82017-09-15 17:58:39 -04003429 boolean mAllCaps = false;
3430 int mShadowColor = 0;
3431 float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0;
3432 boolean mHasElegant = false;
3433 boolean mElegant = false;
Jeff Davidsona192cc22018-02-08 15:30:06 -08003434 boolean mHasFallbackLineSpacing = false;
3435 boolean mFallbackLineSpacing = false;
Justin Klaassen10d07c82017-09-15 17:58:39 -04003436 boolean mHasLetterSpacing = false;
3437 float mLetterSpacing = 0;
3438 String mFontFeatureSettings = null;
3439
3440 @Override
3441 public String toString() {
3442 return "TextAppearanceAttributes {\n"
3443 + " mTextColorHighlight:" + mTextColorHighlight + "\n"
3444 + " mTextColor:" + mTextColor + "\n"
3445 + " mTextColorHint:" + mTextColorHint + "\n"
3446 + " mTextColorLink:" + mTextColorLink + "\n"
3447 + " mTextSize:" + mTextSize + "\n"
3448 + " mFontFamily:" + mFontFamily + "\n"
3449 + " mFontTypeface:" + mFontTypeface + "\n"
3450 + " mFontFamilyExplicit:" + mFontFamilyExplicit + "\n"
3451 + " mTypefaceIndex:" + mTypefaceIndex + "\n"
3452 + " mStyleIndex:" + mStyleIndex + "\n"
Justin Klaassen4d01eea2018-04-03 23:21:57 -04003453 + " mFontWeight:" + mFontWeight + "\n"
Justin Klaassen10d07c82017-09-15 17:58:39 -04003454 + " mAllCaps:" + mAllCaps + "\n"
3455 + " mShadowColor:" + mShadowColor + "\n"
3456 + " mShadowDx:" + mShadowDx + "\n"
3457 + " mShadowDy:" + mShadowDy + "\n"
3458 + " mShadowRadius:" + mShadowRadius + "\n"
3459 + " mHasElegant:" + mHasElegant + "\n"
3460 + " mElegant:" + mElegant + "\n"
Jeff Davidsona192cc22018-02-08 15:30:06 -08003461 + " mHasFallbackLineSpacing:" + mHasFallbackLineSpacing + "\n"
3462 + " mFallbackLineSpacing:" + mFallbackLineSpacing + "\n"
Justin Klaassen10d07c82017-09-15 17:58:39 -04003463 + " mHasLetterSpacing:" + mHasLetterSpacing + "\n"
3464 + " mLetterSpacing:" + mLetterSpacing + "\n"
3465 + " mFontFeatureSettings:" + mFontFeatureSettings + "\n"
3466 + "}";
3467 }
3468 }
3469
3470 // Maps styleable attributes that exist both in TextView style and TextAppearance.
3471 private static final SparseIntArray sAppearanceValues = new SparseIntArray();
3472 static {
3473 sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHighlight,
3474 com.android.internal.R.styleable.TextAppearance_textColorHighlight);
3475 sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColor,
3476 com.android.internal.R.styleable.TextAppearance_textColor);
3477 sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHint,
3478 com.android.internal.R.styleable.TextAppearance_textColorHint);
3479 sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorLink,
3480 com.android.internal.R.styleable.TextAppearance_textColorLink);
3481 sAppearanceValues.put(com.android.internal.R.styleable.TextView_textSize,
3482 com.android.internal.R.styleable.TextAppearance_textSize);
3483 sAppearanceValues.put(com.android.internal.R.styleable.TextView_typeface,
3484 com.android.internal.R.styleable.TextAppearance_typeface);
3485 sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFamily,
3486 com.android.internal.R.styleable.TextAppearance_fontFamily);
3487 sAppearanceValues.put(com.android.internal.R.styleable.TextView_textStyle,
3488 com.android.internal.R.styleable.TextAppearance_textStyle);
Justin Klaassen4d01eea2018-04-03 23:21:57 -04003489 sAppearanceValues.put(com.android.internal.R.styleable.TextView_textFontWeight,
3490 com.android.internal.R.styleable.TextAppearance_textFontWeight);
Justin Klaassen10d07c82017-09-15 17:58:39 -04003491 sAppearanceValues.put(com.android.internal.R.styleable.TextView_textAllCaps,
3492 com.android.internal.R.styleable.TextAppearance_textAllCaps);
3493 sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowColor,
3494 com.android.internal.R.styleable.TextAppearance_shadowColor);
3495 sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowDx,
3496 com.android.internal.R.styleable.TextAppearance_shadowDx);
3497 sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowDy,
3498 com.android.internal.R.styleable.TextAppearance_shadowDy);
3499 sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowRadius,
3500 com.android.internal.R.styleable.TextAppearance_shadowRadius);
3501 sAppearanceValues.put(com.android.internal.R.styleable.TextView_elegantTextHeight,
3502 com.android.internal.R.styleable.TextAppearance_elegantTextHeight);
Jeff Davidsona192cc22018-02-08 15:30:06 -08003503 sAppearanceValues.put(com.android.internal.R.styleable.TextView_fallbackLineSpacing,
3504 com.android.internal.R.styleable.TextAppearance_fallbackLineSpacing);
Justin Klaassen10d07c82017-09-15 17:58:39 -04003505 sAppearanceValues.put(com.android.internal.R.styleable.TextView_letterSpacing,
3506 com.android.internal.R.styleable.TextAppearance_letterSpacing);
3507 sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFeatureSettings,
3508 com.android.internal.R.styleable.TextAppearance_fontFeatureSettings);
3509 }
3510
3511 /**
3512 * Read the Text Appearance attributes from a given TypedArray and set its values to the given
3513 * set. If the TypedArray contains a value that was already set in the given attributes, that
3514 * will be overriden.
3515 *
3516 * @param context The Context to be used
3517 * @param appearance The TypedArray to read properties from
3518 * @param attributes the TextAppearanceAttributes to fill in
3519 * @param styleArray Whether the given TypedArray is a style or a TextAppearance. This defines
3520 * what attribute indexes will be used to read the properties.
3521 */
3522 private void readTextAppearance(Context context, TypedArray appearance,
3523 TextAppearanceAttributes attributes, boolean styleArray) {
3524 final int n = appearance.getIndexCount();
3525 for (int i = 0; i < n; i++) {
3526 final int attr = appearance.getIndex(i);
3527 int index = attr;
3528 // Translate style array index ids to TextAppearance ids.
3529 if (styleArray) {
3530 index = sAppearanceValues.get(attr, -1);
3531 if (index == -1) {
3532 // This value is not part of a Text Appearance and should be ignored.
3533 continue;
3534 }
3535 }
3536 switch (index) {
3537 case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
3538 attributes.mTextColorHighlight =
3539 appearance.getColor(attr, attributes.mTextColorHighlight);
3540 break;
3541 case com.android.internal.R.styleable.TextAppearance_textColor:
3542 attributes.mTextColor = appearance.getColorStateList(attr);
3543 break;
3544 case com.android.internal.R.styleable.TextAppearance_textColorHint:
3545 attributes.mTextColorHint = appearance.getColorStateList(attr);
3546 break;
3547 case com.android.internal.R.styleable.TextAppearance_textColorLink:
3548 attributes.mTextColorLink = appearance.getColorStateList(attr);
3549 break;
3550 case com.android.internal.R.styleable.TextAppearance_textSize:
3551 attributes.mTextSize =
3552 appearance.getDimensionPixelSize(attr, attributes.mTextSize);
3553 break;
3554 case com.android.internal.R.styleable.TextAppearance_typeface:
3555 attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex);
3556 if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
3557 attributes.mFontFamily = null;
3558 }
3559 break;
3560 case com.android.internal.R.styleable.TextAppearance_fontFamily:
3561 if (!context.isRestricted() && context.canLoadUnsafeResources()) {
3562 try {
3563 attributes.mFontTypeface = appearance.getFont(attr);
3564 } catch (UnsupportedOperationException | Resources.NotFoundException e) {
3565 // Expected if it is not a font resource.
3566 }
3567 }
3568 if (attributes.mFontTypeface == null) {
3569 attributes.mFontFamily = appearance.getString(attr);
3570 }
3571 attributes.mFontFamilyExplicit = true;
3572 break;
3573 case com.android.internal.R.styleable.TextAppearance_textStyle:
3574 attributes.mStyleIndex = appearance.getInt(attr, attributes.mStyleIndex);
3575 break;
Justin Klaassen4d01eea2018-04-03 23:21:57 -04003576 case com.android.internal.R.styleable.TextAppearance_textFontWeight:
3577 attributes.mFontWeight = appearance.getInt(attr, attributes.mFontWeight);
3578 break;
Justin Klaassen10d07c82017-09-15 17:58:39 -04003579 case com.android.internal.R.styleable.TextAppearance_textAllCaps:
3580 attributes.mAllCaps = appearance.getBoolean(attr, attributes.mAllCaps);
3581 break;
3582 case com.android.internal.R.styleable.TextAppearance_shadowColor:
3583 attributes.mShadowColor = appearance.getInt(attr, attributes.mShadowColor);
3584 break;
3585 case com.android.internal.R.styleable.TextAppearance_shadowDx:
3586 attributes.mShadowDx = appearance.getFloat(attr, attributes.mShadowDx);
3587 break;
3588 case com.android.internal.R.styleable.TextAppearance_shadowDy:
3589 attributes.mShadowDy = appearance.getFloat(attr, attributes.mShadowDy);
3590 break;
3591 case com.android.internal.R.styleable.TextAppearance_shadowRadius:
3592 attributes.mShadowRadius = appearance.getFloat(attr, attributes.mShadowRadius);
3593 break;
3594 case com.android.internal.R.styleable.TextAppearance_elegantTextHeight:
3595 attributes.mHasElegant = true;
3596 attributes.mElegant = appearance.getBoolean(attr, attributes.mElegant);
3597 break;
Jeff Davidsona192cc22018-02-08 15:30:06 -08003598 case com.android.internal.R.styleable.TextAppearance_fallbackLineSpacing:
3599 attributes.mHasFallbackLineSpacing = true;
3600 attributes.mFallbackLineSpacing = appearance.getBoolean(attr,
3601 attributes.mFallbackLineSpacing);
3602 break;
Justin Klaassen10d07c82017-09-15 17:58:39 -04003603 case com.android.internal.R.styleable.TextAppearance_letterSpacing:
3604 attributes.mHasLetterSpacing = true;
3605 attributes.mLetterSpacing =
3606 appearance.getFloat(attr, attributes.mLetterSpacing);
3607 break;
3608 case com.android.internal.R.styleable.TextAppearance_fontFeatureSettings:
3609 attributes.mFontFeatureSettings = appearance.getString(attr);
3610 break;
3611 default:
3612 }
3613 }
3614 }
3615
3616 private void applyTextAppearance(TextAppearanceAttributes attributes) {
3617 if (attributes.mTextColor != null) {
3618 setTextColor(attributes.mTextColor);
3619 }
3620
3621 if (attributes.mTextColorHint != null) {
3622 setHintTextColor(attributes.mTextColorHint);
3623 }
3624
3625 if (attributes.mTextColorLink != null) {
3626 setLinkTextColor(attributes.mTextColorLink);
3627 }
3628
3629 if (attributes.mTextColorHighlight != 0) {
3630 setHighlightColor(attributes.mTextColorHighlight);
3631 }
3632
3633 if (attributes.mTextSize != 0) {
3634 setRawTextSize(attributes.mTextSize, true /* shouldRequestLayout */);
3635 }
3636
3637 if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
3638 attributes.mFontFamily = null;
3639 }
3640 setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily,
Justin Klaassen4d01eea2018-04-03 23:21:57 -04003641 attributes.mTypefaceIndex, attributes.mStyleIndex, attributes.mFontWeight);
Justin Klaassen10d07c82017-09-15 17:58:39 -04003642
3643 if (attributes.mShadowColor != 0) {
3644 setShadowLayer(attributes.mShadowRadius, attributes.mShadowDx, attributes.mShadowDy,
3645 attributes.mShadowColor);
3646 }
3647
3648 if (attributes.mAllCaps) {
3649 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
3650 }
3651
3652 if (attributes.mHasElegant) {
3653 setElegantTextHeight(attributes.mElegant);
3654 }
3655
Jeff Davidsona192cc22018-02-08 15:30:06 -08003656 if (attributes.mHasFallbackLineSpacing) {
3657 setFallbackLineSpacing(attributes.mFallbackLineSpacing);
3658 }
3659
Justin Klaassen10d07c82017-09-15 17:58:39 -04003660 if (attributes.mHasLetterSpacing) {
3661 setLetterSpacing(attributes.mLetterSpacing);
3662 }
3663
3664 if (attributes.mFontFeatureSettings != null) {
3665 setFontFeatureSettings(attributes.mFontFeatureSettings);
3666 }
3667 }
3668
3669 /**
3670 * Get the default primary {@link Locale} of the text in this TextView. This will always be
3671 * the first member of {@link #getTextLocales()}.
3672 * @return the default primary {@link Locale} of the text in this TextView.
3673 */
3674 @NonNull
3675 public Locale getTextLocale() {
3676 return mTextPaint.getTextLocale();
3677 }
3678
3679 /**
3680 * Get the default {@link LocaleList} of the text in this TextView.
3681 * @return the default {@link LocaleList} of the text in this TextView.
3682 */
3683 @NonNull @Size(min = 1)
3684 public LocaleList getTextLocales() {
3685 return mTextPaint.getTextLocales();
3686 }
3687
3688 private void changeListenerLocaleTo(@Nullable Locale locale) {
3689 if (mListenerChanged) {
3690 // If a listener has been explicitly set, don't change it. We may break something.
3691 return;
3692 }
3693 // The following null check is not absolutely necessary since all calling points of
3694 // changeListenerLocaleTo() guarantee a non-null mEditor at the moment. But this is left
3695 // here in case others would want to call this method in the future.
3696 if (mEditor != null) {
3697 KeyListener listener = mEditor.mKeyListener;
3698 if (listener instanceof DigitsKeyListener) {
3699 listener = DigitsKeyListener.getInstance(locale, (DigitsKeyListener) listener);
3700 } else if (listener instanceof DateKeyListener) {
3701 listener = DateKeyListener.getInstance(locale);
3702 } else if (listener instanceof TimeKeyListener) {
3703 listener = TimeKeyListener.getInstance(locale);
3704 } else if (listener instanceof DateTimeKeyListener) {
3705 listener = DateTimeKeyListener.getInstance(locale);
3706 } else {
3707 return;
3708 }
3709 final boolean wasPasswordType = isPasswordInputType(mEditor.mInputType);
3710 setKeyListenerOnly(listener);
3711 setInputTypeFromEditor();
3712 if (wasPasswordType) {
3713 final int newInputClass = mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS;
3714 if (newInputClass == EditorInfo.TYPE_CLASS_TEXT) {
3715 mEditor.mInputType |= EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
3716 } else if (newInputClass == EditorInfo.TYPE_CLASS_NUMBER) {
3717 mEditor.mInputType |= EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
3718 }
3719 }
3720 }
3721 }
3722
3723 /**
3724 * Set the default {@link Locale} of the text in this TextView to a one-member
3725 * {@link LocaleList} containing just the given Locale.
3726 *
3727 * @param locale the {@link Locale} for drawing text, must not be null.
3728 *
3729 * @see #setTextLocales
3730 */
3731 public void setTextLocale(@NonNull Locale locale) {
3732 mLocalesChanged = true;
3733 mTextPaint.setTextLocale(locale);
3734 if (mLayout != null) {
3735 nullLayouts();
3736 requestLayout();
3737 invalidate();
3738 }
3739 }
3740
3741 /**
3742 * Set the default {@link LocaleList} of the text in this TextView to the given value.
3743 *
3744 * This value is used to choose appropriate typefaces for ambiguous characters (typically used
3745 * for CJK locales to disambiguate Hanzi/Kanji/Hanja characters). It also affects
3746 * other aspects of text display, including line breaking.
3747 *
3748 * @param locales the {@link LocaleList} for drawing text, must not be null or empty.
3749 *
3750 * @see Paint#setTextLocales
3751 */
3752 public void setTextLocales(@NonNull @Size(min = 1) LocaleList locales) {
3753 mLocalesChanged = true;
3754 mTextPaint.setTextLocales(locales);
3755 if (mLayout != null) {
3756 nullLayouts();
3757 requestLayout();
3758 invalidate();
3759 }
3760 }
3761
3762 @Override
3763 protected void onConfigurationChanged(Configuration newConfig) {
3764 super.onConfigurationChanged(newConfig);
3765 if (!mLocalesChanged) {
3766 mTextPaint.setTextLocales(LocaleList.getDefault());
3767 if (mLayout != null) {
3768 nullLayouts();
3769 requestLayout();
3770 invalidate();
3771 }
3772 }
3773 }
3774
3775 /**
3776 * @return the size (in pixels) of the default text size in this TextView.
3777 */
3778 @ViewDebug.ExportedProperty(category = "text")
3779 public float getTextSize() {
3780 return mTextPaint.getTextSize();
3781 }
3782
3783 /**
3784 * @return the size (in scaled pixels) of the default text size in this TextView.
3785 * @hide
3786 */
3787 @ViewDebug.ExportedProperty(category = "text")
3788 public float getScaledTextSize() {
3789 return mTextPaint.getTextSize() / mTextPaint.density;
3790 }
3791
3792 /** @hide */
3793 @ViewDebug.ExportedProperty(category = "text", mapping = {
3794 @ViewDebug.IntToString(from = Typeface.NORMAL, to = "NORMAL"),
3795 @ViewDebug.IntToString(from = Typeface.BOLD, to = "BOLD"),
3796 @ViewDebug.IntToString(from = Typeface.ITALIC, to = "ITALIC"),
3797 @ViewDebug.IntToString(from = Typeface.BOLD_ITALIC, to = "BOLD_ITALIC")
3798 })
3799 public int getTypefaceStyle() {
3800 Typeface typeface = mTextPaint.getTypeface();
3801 return typeface != null ? typeface.getStyle() : Typeface.NORMAL;
3802 }
3803
3804 /**
3805 * Set the default text size to the given value, interpreted as "scaled
3806 * pixel" units. This size is adjusted based on the current density and
3807 * user font size preference.
3808 *
3809 * <p>Note: if this TextView has the auto-size feature enabled than this function is no-op.
3810 *
3811 * @param size The scaled pixel size.
3812 *
3813 * @attr ref android.R.styleable#TextView_textSize
3814 */
3815 @android.view.RemotableViewMethod
3816 public void setTextSize(float size) {
3817 setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
3818 }
3819
3820 /**
3821 * Set the default text size to a given unit and value. See {@link
3822 * TypedValue} for the possible dimension units.
3823 *
3824 * <p>Note: if this TextView has the auto-size feature enabled than this function is no-op.
3825 *
3826 * @param unit The desired dimension unit.
3827 * @param size The desired size in the given units.
3828 *
3829 * @attr ref android.R.styleable#TextView_textSize
3830 */
3831 public void setTextSize(int unit, float size) {
3832 if (!isAutoSizeEnabled()) {
3833 setTextSizeInternal(unit, size, true /* shouldRequestLayout */);
3834 }
3835 }
3836
3837 private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) {
3838 Context c = getContext();
3839 Resources r;
3840
3841 if (c == null) {
3842 r = Resources.getSystem();
3843 } else {
3844 r = c.getResources();
3845 }
3846
3847 setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()),
3848 shouldRequestLayout);
3849 }
3850
3851 private void setRawTextSize(float size, boolean shouldRequestLayout) {
3852 if (size != mTextPaint.getTextSize()) {
3853 mTextPaint.setTextSize(size);
3854
3855 if (shouldRequestLayout && mLayout != null) {
3856 // Do not auto-size right after setting the text size.
3857 mNeedsAutoSizeText = false;
3858 nullLayouts();
3859 requestLayout();
3860 invalidate();
3861 }
3862 }
3863 }
3864
3865 /**
3866 * Gets the extent by which text should be stretched horizontally.
3867 * This will usually be 1.0.
3868 * @return The horizontal scale factor.
3869 */
3870 public float getTextScaleX() {
3871 return mTextPaint.getTextScaleX();
3872 }
3873
3874 /**
3875 * Sets the horizontal scale factor for text. The default value
3876 * is 1.0. Values greater than 1.0 stretch the text wider.
3877 * Values less than 1.0 make the text narrower. By default, this value is 1.0.
3878 * @param size The horizontal scale factor.
3879 * @attr ref android.R.styleable#TextView_textScaleX
3880 */
3881 @android.view.RemotableViewMethod
3882 public void setTextScaleX(float size) {
3883 if (size != mTextPaint.getTextScaleX()) {
3884 mUserSetTextScaleX = true;
3885 mTextPaint.setTextScaleX(size);
3886
3887 if (mLayout != null) {
3888 nullLayouts();
3889 requestLayout();
3890 invalidate();
3891 }
3892 }
3893 }
3894
3895 /**
3896 * Sets the typeface and style in which the text should be displayed.
3897 * Note that not all Typeface families actually have bold and italic
3898 * variants, so you may need to use
3899 * {@link #setTypeface(Typeface, int)} to get the appearance
3900 * that you actually want.
3901 *
3902 * @see #getTypeface()
3903 *
3904 * @attr ref android.R.styleable#TextView_fontFamily
3905 * @attr ref android.R.styleable#TextView_typeface
3906 * @attr ref android.R.styleable#TextView_textStyle
3907 */
Justin Klaassen4d01eea2018-04-03 23:21:57 -04003908 public void setTypeface(@Nullable Typeface tf) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04003909 if (mTextPaint.getTypeface() != tf) {
3910 mTextPaint.setTypeface(tf);
3911
3912 if (mLayout != null) {
3913 nullLayouts();
3914 requestLayout();
3915 invalidate();
3916 }
3917 }
3918 }
3919
3920 /**
3921 * Gets the current {@link Typeface} that is used to style the text.
3922 * @return The current Typeface.
3923 *
3924 * @see #setTypeface(Typeface)
3925 *
3926 * @attr ref android.R.styleable#TextView_fontFamily
3927 * @attr ref android.R.styleable#TextView_typeface
3928 * @attr ref android.R.styleable#TextView_textStyle
3929 */
3930 public Typeface getTypeface() {
3931 return mTextPaint.getTypeface();
3932 }
3933
3934 /**
3935 * Set the TextView's elegant height metrics flag. This setting selects font
3936 * variants that have not been compacted to fit Latin-based vertical
3937 * metrics, and also increases top and bottom bounds to provide more space.
3938 *
3939 * @param elegant set the paint's elegant metrics flag.
3940 *
Jeff Davidsona192cc22018-02-08 15:30:06 -08003941 * @see #isElegantTextHeight()
3942 * @see Paint#isElegantTextHeight()
Justin Klaassen10d07c82017-09-15 17:58:39 -04003943 *
3944 * @attr ref android.R.styleable#TextView_elegantTextHeight
3945 */
3946 public void setElegantTextHeight(boolean elegant) {
3947 if (elegant != mTextPaint.isElegantTextHeight()) {
3948 mTextPaint.setElegantTextHeight(elegant);
3949 if (mLayout != null) {
3950 nullLayouts();
3951 requestLayout();
3952 invalidate();
3953 }
3954 }
3955 }
3956
3957 /**
Jeff Davidsona192cc22018-02-08 15:30:06 -08003958 * Set whether to respect the ascent and descent of the fallback fonts that are used in
3959 * displaying the text (which is needed to avoid text from consecutive lines running into
3960 * each other). If set, fallback fonts that end up getting used can increase the ascent
3961 * and descent of the lines that they are used on.
3962 * <p/>
3963 * It is required to be true if text could be in languages like Burmese or Tibetan where text
3964 * is typically much taller or deeper than Latin text.
3965 *
3966 * @param enabled whether to expand linespacing based on fallback fonts, {@code true} by default
3967 *
3968 * @see StaticLayout.Builder#setUseLineSpacingFromFallbacks(boolean)
3969 *
3970 * @attr ref android.R.styleable#TextView_fallbackLineSpacing
3971 */
3972 public void setFallbackLineSpacing(boolean enabled) {
3973 if (mUseFallbackLineSpacing != enabled) {
3974 mUseFallbackLineSpacing = enabled;
3975 if (mLayout != null) {
3976 nullLayouts();
3977 requestLayout();
3978 invalidate();
3979 }
3980 }
3981 }
3982
3983 /**
3984 * @return whether fallback line spacing is enabled, {@code true} by default
3985 *
3986 * @see #setFallbackLineSpacing(boolean)
3987 *
3988 * @attr ref android.R.styleable#TextView_fallbackLineSpacing
3989 */
3990 public boolean isFallbackLineSpacing() {
3991 return mUseFallbackLineSpacing;
3992 }
3993
3994 /**
Justin Klaassen10d07c82017-09-15 17:58:39 -04003995 * Get the value of the TextView's elegant height metrics flag. This setting selects font
3996 * variants that have not been compacted to fit Latin-based vertical
3997 * metrics, and also increases top and bottom bounds to provide more space.
3998 * @return {@code true} if the elegant height metrics flag is set.
3999 *
4000 * @see #setElegantTextHeight(boolean)
4001 * @see Paint#setElegantTextHeight(boolean)
4002 */
4003 public boolean isElegantTextHeight() {
4004 return mTextPaint.isElegantTextHeight();
4005 }
4006
4007 /**
4008 * Gets the text letter-space value, which determines the spacing between characters.
4009 * The value returned is in ems. Normally, this value is 0.0.
4010 * @return The text letter-space value in ems.
4011 *
4012 * @see #setLetterSpacing(float)
4013 * @see Paint#setLetterSpacing
4014 */
4015 public float getLetterSpacing() {
4016 return mTextPaint.getLetterSpacing();
4017 }
4018
4019 /**
4020 * Sets text letter-spacing in em units. Typical values
4021 * for slight expansion will be around 0.05. Negative values tighten text.
4022 *
4023 * @see #getLetterSpacing()
4024 * @see Paint#getLetterSpacing
4025 *
4026 * @param letterSpacing A text letter-space value in ems.
4027 * @attr ref android.R.styleable#TextView_letterSpacing
4028 */
4029 @android.view.RemotableViewMethod
4030 public void setLetterSpacing(float letterSpacing) {
4031 if (letterSpacing != mTextPaint.getLetterSpacing()) {
4032 mTextPaint.setLetterSpacing(letterSpacing);
4033
4034 if (mLayout != null) {
4035 nullLayouts();
4036 requestLayout();
4037 invalidate();
4038 }
4039 }
4040 }
4041
4042 /**
4043 * Returns the font feature settings. The format is the same as the CSS
4044 * font-feature-settings attribute:
4045 * <a href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop">
4046 * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a>
4047 *
4048 * @return the currently set font feature settings. Default is null.
4049 *
4050 * @see #setFontFeatureSettings(String)
4051 * @see Paint#setFontFeatureSettings(String) Paint.setFontFeatureSettings(String)
4052 */
4053 @Nullable
4054 public String getFontFeatureSettings() {
4055 return mTextPaint.getFontFeatureSettings();
4056 }
4057
4058 /**
4059 * Returns the font variation settings.
4060 *
4061 * @return the currently set font variation settings. Returns null if no variation is
4062 * specified.
4063 *
4064 * @see #setFontVariationSettings(String)
4065 * @see Paint#setFontVariationSettings(String) Paint.setFontVariationSettings(String)
4066 */
4067 @Nullable
4068 public String getFontVariationSettings() {
4069 return mTextPaint.getFontVariationSettings();
4070 }
4071
4072 /**
4073 * Sets the break strategy for breaking paragraphs into lines. The default value for
4074 * TextView is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}, and the default value for
4075 * EditText is {@link Layout#BREAK_STRATEGY_SIMPLE}, the latter to avoid the
4076 * text "dancing" when being edited.
4077 *
4078 * @attr ref android.R.styleable#TextView_breakStrategy
4079 * @see #getBreakStrategy()
4080 */
4081 public void setBreakStrategy(@Layout.BreakStrategy int breakStrategy) {
4082 mBreakStrategy = breakStrategy;
4083 if (mLayout != null) {
4084 nullLayouts();
4085 requestLayout();
4086 invalidate();
4087 }
4088 }
4089
4090 /**
4091 * Gets the current strategy for breaking paragraphs into lines.
4092 * @return the current strategy for breaking paragraphs into lines.
4093 *
4094 * @attr ref android.R.styleable#TextView_breakStrategy
4095 * @see #setBreakStrategy(int)
4096 */
4097 @Layout.BreakStrategy
4098 public int getBreakStrategy() {
4099 return mBreakStrategy;
4100 }
4101
4102 /**
4103 * Sets the frequency of automatic hyphenation to use when determining word breaks.
4104 * The default value for both TextView and {@link EditText} is
4105 * {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
4106 * Note that the default hyphenation frequency value is set from the theme.
4107 *
4108 * @param hyphenationFrequency The hyphenation frequency to use.
4109 * @attr ref android.R.styleable#TextView_hyphenationFrequency
4110 * @see #getHyphenationFrequency()
4111 */
4112 public void setHyphenationFrequency(@Layout.HyphenationFrequency int hyphenationFrequency) {
4113 mHyphenationFrequency = hyphenationFrequency;
4114 if (mLayout != null) {
4115 nullLayouts();
4116 requestLayout();
4117 invalidate();
4118 }
4119 }
4120
4121 /**
4122 * Gets the current frequency of automatic hyphenation to be used when determining word breaks.
4123 * @return the current frequency of automatic hyphenation to be used when determining word
4124 * breaks.
4125 *
4126 * @attr ref android.R.styleable#TextView_hyphenationFrequency
4127 * @see #setHyphenationFrequency(int)
4128 */
4129 @Layout.HyphenationFrequency
4130 public int getHyphenationFrequency() {
4131 return mHyphenationFrequency;
4132 }
4133
4134 /**
Justin Klaassen4d01eea2018-04-03 23:21:57 -04004135 * Gets the parameters for text layout precomputation, for use with {@link PrecomputedText}.
4136 *
4137 * @return a current {@link PrecomputedText.Params}
4138 * @see PrecomputedText
4139 */
4140 public @NonNull PrecomputedText.Params getTextMetricsParams() {
4141 return new PrecomputedText.Params(new TextPaint(mTextPaint), getTextDirectionHeuristic(),
4142 mBreakStrategy, mHyphenationFrequency);
4143 }
4144
4145 /**
4146 * Apply the text layout parameter.
4147 *
4148 * Update the TextView parameters to be compatible with {@link PrecomputedText.Params}.
4149 * @see PrecomputedText
4150 */
4151 public void setTextMetricsParams(@NonNull PrecomputedText.Params params) {
4152 mTextPaint.set(params.getTextPaint());
4153 mUserSetTextScaleX = true;
4154 mTextDir = params.getTextDirection();
4155 mBreakStrategy = params.getBreakStrategy();
4156 mHyphenationFrequency = params.getHyphenationFrequency();
4157 if (mLayout != null) {
4158 nullLayouts();
4159 requestLayout();
4160 invalidate();
4161 }
4162 }
4163
4164 /**
Justin Klaassen10d07c82017-09-15 17:58:39 -04004165 * Set justification mode. The default value is {@link Layout#JUSTIFICATION_MODE_NONE}. If the
4166 * last line is too short for justification, the last line will be displayed with the
4167 * alignment set by {@link android.view.View#setTextAlignment}.
4168 *
4169 * @see #getJustificationMode()
4170 */
4171 @Layout.JustificationMode
4172 public void setJustificationMode(@Layout.JustificationMode int justificationMode) {
4173 mJustificationMode = justificationMode;
4174 if (mLayout != null) {
4175 nullLayouts();
4176 requestLayout();
4177 invalidate();
4178 }
4179 }
4180
4181 /**
4182 * @return true if currently paragraph justification mode.
4183 *
4184 * @see #setJustificationMode(int)
4185 */
4186 public @Layout.JustificationMode int getJustificationMode() {
4187 return mJustificationMode;
4188 }
4189
4190 /**
4191 * Sets font feature settings. The format is the same as the CSS
4192 * font-feature-settings attribute:
4193 * <a href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop">
4194 * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a>
4195 *
4196 * @param fontFeatureSettings font feature settings represented as CSS compatible string
4197 *
4198 * @see #getFontFeatureSettings()
4199 * @see Paint#getFontFeatureSettings() Paint.getFontFeatureSettings()
4200 *
4201 * @attr ref android.R.styleable#TextView_fontFeatureSettings
4202 */
4203 @android.view.RemotableViewMethod
4204 public void setFontFeatureSettings(@Nullable String fontFeatureSettings) {
4205 if (fontFeatureSettings != mTextPaint.getFontFeatureSettings()) {
4206 mTextPaint.setFontFeatureSettings(fontFeatureSettings);
4207
4208 if (mLayout != null) {
4209 nullLayouts();
4210 requestLayout();
4211 invalidate();
4212 }
4213 }
4214 }
4215
4216
4217 /**
4218 * Sets TrueType or OpenType font variation settings. The settings string is constructed from
4219 * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
4220 * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
4221 * are longer or shorter than four characters, or contain characters outside of U+0020..U+007E
4222 * are invalid. If a specified axis name is not defined in the font, the settings will be
4223 * ignored.
4224 *
4225 * <p>
4226 * Examples,
4227 * <ul>
4228 * <li>Set font width to 150.
4229 * <pre>
4230 * <code>
4231 * TextView textView = (TextView) findViewById(R.id.textView);
4232 * textView.setFontVariationSettings("'wdth' 150");
4233 * </code>
4234 * </pre>
4235 * </li>
4236 *
4237 * <li>Set the font slant to 20 degrees and ask for italic style.
4238 * <pre>
4239 * <code>
4240 * TextView textView = (TextView) findViewById(R.id.textView);
4241 * textView.setFontVariationSettings("'slnt' 20, 'ital' 1");
4242 * </code>
4243 * </pre>
4244 * </p>
4245 * </li>
4246 * </ul>
4247 *
4248 * @param fontVariationSettings font variation settings. You can pass null or empty string as
4249 * no variation settings.
4250 * @return true if the given settings is effective to at least one font file underlying this
4251 * TextView. This function also returns true for empty settings string. Otherwise
4252 * returns false.
4253 *
4254 * @throws IllegalArgumentException If given string is not a valid font variation settings
4255 * format.
4256 *
4257 * @see #getFontVariationSettings()
4258 * @see FontVariationAxis
4259 */
4260 public boolean setFontVariationSettings(@Nullable String fontVariationSettings) {
4261 final String existingSettings = mTextPaint.getFontVariationSettings();
4262 if (fontVariationSettings == existingSettings
4263 || (fontVariationSettings != null
4264 && fontVariationSettings.equals(existingSettings))) {
4265 return true;
4266 }
4267 boolean effective = mTextPaint.setFontVariationSettings(fontVariationSettings);
4268
4269 if (effective && mLayout != null) {
4270 nullLayouts();
4271 requestLayout();
4272 invalidate();
4273 }
4274 return effective;
4275 }
4276
4277 /**
4278 * Sets the text color for all the states (normal, selected,
4279 * focused) to be this color.
4280 *
4281 * @param color A color value in the form 0xAARRGGBB.
4282 * Do not pass a resource ID. To get a color value from a resource ID, call
4283 * {@link android.support.v4.content.ContextCompat#getColor(Context, int) getColor}.
4284 *
4285 * @see #setTextColor(ColorStateList)
4286 * @see #getTextColors()
4287 *
4288 * @attr ref android.R.styleable#TextView_textColor
4289 */
4290 @android.view.RemotableViewMethod
4291 public void setTextColor(@ColorInt int color) {
4292 mTextColor = ColorStateList.valueOf(color);
4293 updateTextColors();
4294 }
4295
4296 /**
4297 * Sets the text color.
4298 *
4299 * @see #setTextColor(int)
4300 * @see #getTextColors()
4301 * @see #setHintTextColor(ColorStateList)
4302 * @see #setLinkTextColor(ColorStateList)
4303 *
4304 * @attr ref android.R.styleable#TextView_textColor
4305 */
4306 @android.view.RemotableViewMethod
4307 public void setTextColor(ColorStateList colors) {
4308 if (colors == null) {
4309 throw new NullPointerException();
4310 }
4311
4312 mTextColor = colors;
4313 updateTextColors();
4314 }
4315
4316 /**
4317 * Gets the text colors for the different states (normal, selected, focused) of the TextView.
4318 *
4319 * @see #setTextColor(ColorStateList)
4320 * @see #setTextColor(int)
4321 *
4322 * @attr ref android.R.styleable#TextView_textColor
4323 */
4324 public final ColorStateList getTextColors() {
4325 return mTextColor;
4326 }
4327
4328 /**
4329 * Return the current color selected for normal text.
4330 *
4331 * @return Returns the current text color.
4332 */
4333 @ColorInt
4334 public final int getCurrentTextColor() {
4335 return mCurTextColor;
4336 }
4337
4338 /**
4339 * Sets the color used to display the selection highlight.
4340 *
4341 * @attr ref android.R.styleable#TextView_textColorHighlight
4342 */
4343 @android.view.RemotableViewMethod
4344 public void setHighlightColor(@ColorInt int color) {
4345 if (mHighlightColor != color) {
4346 mHighlightColor = color;
4347 invalidate();
4348 }
4349 }
4350
4351 /**
4352 * @return the color used to display the selection highlight
4353 *
4354 * @see #setHighlightColor(int)
4355 *
4356 * @attr ref android.R.styleable#TextView_textColorHighlight
4357 */
4358 @ColorInt
4359 public int getHighlightColor() {
4360 return mHighlightColor;
4361 }
4362
4363 /**
4364 * Sets whether the soft input method will be made visible when this
4365 * TextView gets focused. The default is true.
4366 */
4367 @android.view.RemotableViewMethod
4368 public final void setShowSoftInputOnFocus(boolean show) {
4369 createEditorIfNeeded();
4370 mEditor.mShowSoftInputOnFocus = show;
4371 }
4372
4373 /**
4374 * Returns whether the soft input method will be made visible when this
4375 * TextView gets focused. The default is true.
4376 */
4377 public final boolean getShowSoftInputOnFocus() {
4378 // When there is no Editor, return default true value
4379 return mEditor == null || mEditor.mShowSoftInputOnFocus;
4380 }
4381
4382 /**
4383 * Gives the text a shadow of the specified blur radius and color, the specified
4384 * distance from its drawn position.
4385 * <p>
4386 * The text shadow produced does not interact with the properties on view
4387 * that are responsible for real time shadows,
4388 * {@link View#getElevation() elevation} and
4389 * {@link View#getTranslationZ() translationZ}.
4390 *
4391 * @see Paint#setShadowLayer(float, float, float, int)
4392 *
4393 * @attr ref android.R.styleable#TextView_shadowColor
4394 * @attr ref android.R.styleable#TextView_shadowDx
4395 * @attr ref android.R.styleable#TextView_shadowDy
4396 * @attr ref android.R.styleable#TextView_shadowRadius
4397 */
4398 public void setShadowLayer(float radius, float dx, float dy, int color) {
4399 mTextPaint.setShadowLayer(radius, dx, dy, color);
4400
4401 mShadowRadius = radius;
4402 mShadowDx = dx;
4403 mShadowDy = dy;
4404 mShadowColor = color;
4405
4406 // Will change text clip region
4407 if (mEditor != null) {
4408 mEditor.invalidateTextDisplayList();
4409 mEditor.invalidateHandlesAndActionMode();
4410 }
4411 invalidate();
4412 }
4413
4414 /**
4415 * Gets the radius of the shadow layer.
4416 *
4417 * @return the radius of the shadow layer. If 0, the shadow layer is not visible
4418 *
4419 * @see #setShadowLayer(float, float, float, int)
4420 *
4421 * @attr ref android.R.styleable#TextView_shadowRadius
4422 */
4423 public float getShadowRadius() {
4424 return mShadowRadius;
4425 }
4426
4427 /**
4428 * @return the horizontal offset of the shadow layer
4429 *
4430 * @see #setShadowLayer(float, float, float, int)
4431 *
4432 * @attr ref android.R.styleable#TextView_shadowDx
4433 */
4434 public float getShadowDx() {
4435 return mShadowDx;
4436 }
4437
4438 /**
4439 * Gets the vertical offset of the shadow layer.
4440 * @return The vertical offset of the shadow layer.
4441 *
4442 * @see #setShadowLayer(float, float, float, int)
4443 *
4444 * @attr ref android.R.styleable#TextView_shadowDy
4445 */
4446 public float getShadowDy() {
4447 return mShadowDy;
4448 }
4449
4450 /**
4451 * Gets the color of the shadow layer.
4452 * @return the color of the shadow layer
4453 *
4454 * @see #setShadowLayer(float, float, float, int)
4455 *
4456 * @attr ref android.R.styleable#TextView_shadowColor
4457 */
4458 @ColorInt
4459 public int getShadowColor() {
4460 return mShadowColor;
4461 }
4462
4463 /**
4464 * Gets the {@link TextPaint} used for the text.
4465 * Use this only to consult the Paint's properties and not to change them.
4466 * @return The base paint used for the text.
4467 */
4468 public TextPaint getPaint() {
4469 return mTextPaint;
4470 }
4471
4472 /**
4473 * Sets the autolink mask of the text. See {@link
4474 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
4475 * possible values.
4476 *
4477 * @attr ref android.R.styleable#TextView_autoLink
4478 */
4479 @android.view.RemotableViewMethod
4480 public final void setAutoLinkMask(int mask) {
4481 mAutoLinkMask = mask;
4482 }
4483
4484 /**
4485 * Sets whether the movement method will automatically be set to
4486 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
4487 * set to nonzero and links are detected in {@link #setText}.
4488 * The default is true.
4489 *
4490 * @attr ref android.R.styleable#TextView_linksClickable
4491 */
4492 @android.view.RemotableViewMethod
4493 public final void setLinksClickable(boolean whether) {
4494 mLinksClickable = whether;
4495 }
4496
4497 /**
4498 * Returns whether the movement method will automatically be set to
4499 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
4500 * set to nonzero and links are detected in {@link #setText}.
4501 * The default is true.
4502 *
4503 * @attr ref android.R.styleable#TextView_linksClickable
4504 */
4505 public final boolean getLinksClickable() {
4506 return mLinksClickable;
4507 }
4508
4509 /**
4510 * Returns the list of {@link android.text.style.URLSpan URLSpans} attached to the text
4511 * (by {@link Linkify} or otherwise) if any. You can call
4512 * {@link URLSpan#getURL} on them to find where they link to
4513 * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
4514 * to find the region of the text they are attached to.
4515 */
4516 public URLSpan[] getUrls() {
4517 if (mText instanceof Spanned) {
4518 return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
4519 } else {
4520 return new URLSpan[0];
4521 }
4522 }
4523
4524 /**
4525 * Sets the color of the hint text for all the states (disabled, focussed, selected...) of this
4526 * TextView.
4527 *
4528 * @see #setHintTextColor(ColorStateList)
4529 * @see #getHintTextColors()
4530 * @see #setTextColor(int)
4531 *
4532 * @attr ref android.R.styleable#TextView_textColorHint
4533 */
4534 @android.view.RemotableViewMethod
4535 public final void setHintTextColor(@ColorInt int color) {
4536 mHintTextColor = ColorStateList.valueOf(color);
4537 updateTextColors();
4538 }
4539
4540 /**
4541 * Sets the color of the hint text.
4542 *
4543 * @see #getHintTextColors()
4544 * @see #setHintTextColor(int)
4545 * @see #setTextColor(ColorStateList)
4546 * @see #setLinkTextColor(ColorStateList)
4547 *
4548 * @attr ref android.R.styleable#TextView_textColorHint
4549 */
4550 public final void setHintTextColor(ColorStateList colors) {
4551 mHintTextColor = colors;
4552 updateTextColors();
4553 }
4554
4555 /**
4556 * @return the color of the hint text, for the different states of this TextView.
4557 *
4558 * @see #setHintTextColor(ColorStateList)
4559 * @see #setHintTextColor(int)
4560 * @see #setTextColor(ColorStateList)
4561 * @see #setLinkTextColor(ColorStateList)
4562 *
4563 * @attr ref android.R.styleable#TextView_textColorHint
4564 */
4565 public final ColorStateList getHintTextColors() {
4566 return mHintTextColor;
4567 }
4568
4569 /**
4570 * <p>Return the current color selected to paint the hint text.</p>
4571 *
4572 * @return Returns the current hint text color.
4573 */
4574 @ColorInt
4575 public final int getCurrentHintTextColor() {
4576 return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
4577 }
4578
4579 /**
4580 * Sets the color of links in the text.
4581 *
4582 * @see #setLinkTextColor(ColorStateList)
4583 * @see #getLinkTextColors()
4584 *
4585 * @attr ref android.R.styleable#TextView_textColorLink
4586 */
4587 @android.view.RemotableViewMethod
4588 public final void setLinkTextColor(@ColorInt int color) {
4589 mLinkTextColor = ColorStateList.valueOf(color);
4590 updateTextColors();
4591 }
4592
4593 /**
4594 * Sets the color of links in the text.
4595 *
4596 * @see #setLinkTextColor(int)
4597 * @see #getLinkTextColors()
4598 * @see #setTextColor(ColorStateList)
4599 * @see #setHintTextColor(ColorStateList)
4600 *
4601 * @attr ref android.R.styleable#TextView_textColorLink
4602 */
4603 public final void setLinkTextColor(ColorStateList colors) {
4604 mLinkTextColor = colors;
4605 updateTextColors();
4606 }
4607
4608 /**
4609 * @return the list of colors used to paint the links in the text, for the different states of
4610 * this TextView
4611 *
4612 * @see #setLinkTextColor(ColorStateList)
4613 * @see #setLinkTextColor(int)
4614 *
4615 * @attr ref android.R.styleable#TextView_textColorLink
4616 */
4617 public final ColorStateList getLinkTextColors() {
4618 return mLinkTextColor;
4619 }
4620
4621 /**
4622 * Sets the horizontal alignment of the text and the
4623 * vertical gravity that will be used when there is extra space
4624 * in the TextView beyond what is required for the text itself.
4625 *
4626 * @see android.view.Gravity
4627 * @attr ref android.R.styleable#TextView_gravity
4628 */
4629 public void setGravity(int gravity) {
4630 if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
4631 gravity |= Gravity.START;
4632 }
4633 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
4634 gravity |= Gravity.TOP;
4635 }
4636
4637 boolean newLayout = false;
4638
4639 if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
4640 != (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) {
4641 newLayout = true;
4642 }
4643
4644 if (gravity != mGravity) {
4645 invalidate();
4646 }
4647
4648 mGravity = gravity;
4649
4650 if (mLayout != null && newLayout) {
4651 // XXX this is heavy-handed because no actual content changes.
4652 int want = mLayout.getWidth();
4653 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
4654
4655 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
4656 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), true);
4657 }
4658 }
4659
4660 /**
4661 * Returns the horizontal and vertical alignment of this TextView.
4662 *
4663 * @see android.view.Gravity
4664 * @attr ref android.R.styleable#TextView_gravity
4665 */
4666 public int getGravity() {
4667 return mGravity;
4668 }
4669
4670 /**
4671 * Gets the flags on the Paint being used to display the text.
4672 * @return The flags on the Paint being used to display the text.
4673 * @see Paint#getFlags
4674 */
4675 public int getPaintFlags() {
4676 return mTextPaint.getFlags();
4677 }
4678
4679 /**
4680 * Sets flags on the Paint being used to display the text and
4681 * reflows the text if they are different from the old flags.
4682 * @see Paint#setFlags
4683 */
4684 @android.view.RemotableViewMethod
4685 public void setPaintFlags(int flags) {
4686 if (mTextPaint.getFlags() != flags) {
4687 mTextPaint.setFlags(flags);
4688
4689 if (mLayout != null) {
4690 nullLayouts();
4691 requestLayout();
4692 invalidate();
4693 }
4694 }
4695 }
4696
4697 /**
4698 * Sets whether the text should be allowed to be wider than the
4699 * View is. If false, it will be wrapped to the width of the View.
4700 *
4701 * @attr ref android.R.styleable#TextView_scrollHorizontally
4702 */
4703 public void setHorizontallyScrolling(boolean whether) {
4704 if (mHorizontallyScrolling != whether) {
4705 mHorizontallyScrolling = whether;
4706
4707 if (mLayout != null) {
4708 nullLayouts();
4709 requestLayout();
4710 invalidate();
4711 }
4712 }
4713 }
4714
4715 /**
4716 * Returns whether the text is allowed to be wider than the View is.
4717 * If false, the text will be wrapped to the width of the View.
4718 *
4719 * @attr ref android.R.styleable#TextView_scrollHorizontally
4720 * @hide
4721 */
4722 public boolean getHorizontallyScrolling() {
4723 return mHorizontallyScrolling;
4724 }
4725
4726 /**
4727 * Sets the height of the TextView to be at least {@code minLines} tall.
4728 * <p>
4729 * This value is used for height calculation if LayoutParams does not force TextView to have an
4730 * exact height. Setting this value overrides other previous minimum height configurations such
4731 * as {@link #setMinHeight(int)} or {@link #setHeight(int)}. {@link #setSingleLine()} will set
4732 * this value to 1.
4733 *
4734 * @param minLines the minimum height of TextView in terms of number of lines
4735 *
4736 * @see #getMinLines()
4737 * @see #setLines(int)
4738 *
4739 * @attr ref android.R.styleable#TextView_minLines
4740 */
4741 @android.view.RemotableViewMethod
4742 public void setMinLines(int minLines) {
4743 mMinimum = minLines;
4744 mMinMode = LINES;
4745
4746 requestLayout();
4747 invalidate();
4748 }
4749
4750 /**
4751 * Returns the minimum height of TextView in terms of number of lines or -1 if the minimum
4752 * height was set using {@link #setMinHeight(int)} or {@link #setHeight(int)}.
4753 *
4754 * @return the minimum height of TextView in terms of number of lines or -1 if the minimum
4755 * height is not defined in lines
4756 *
4757 * @see #setMinLines(int)
4758 * @see #setLines(int)
4759 *
4760 * @attr ref android.R.styleable#TextView_minLines
4761 */
4762 public int getMinLines() {
4763 return mMinMode == LINES ? mMinimum : -1;
4764 }
4765
4766 /**
4767 * Sets the height of the TextView to be at least {@code minPixels} tall.
4768 * <p>
4769 * This value is used for height calculation if LayoutParams does not force TextView to have an
4770 * exact height. Setting this value overrides previous minimum height configurations such as
4771 * {@link #setMinLines(int)} or {@link #setLines(int)}.
4772 * <p>
4773 * The value given here is different than {@link #setMinimumHeight(int)}. Between
4774 * {@code minHeight} and the value set in {@link #setMinimumHeight(int)}, the greater one is
4775 * used to decide the final height.
4776 *
4777 * @param minPixels the minimum height of TextView in terms of pixels
4778 *
4779 * @see #getMinHeight()
4780 * @see #setHeight(int)
4781 *
4782 * @attr ref android.R.styleable#TextView_minHeight
4783 */
4784 @android.view.RemotableViewMethod
4785 public void setMinHeight(int minPixels) {
4786 mMinimum = minPixels;
4787 mMinMode = PIXELS;
4788
4789 requestLayout();
4790 invalidate();
4791 }
4792
4793 /**
4794 * Returns the minimum height of TextView in terms of pixels or -1 if the minimum height was
4795 * set using {@link #setMinLines(int)} or {@link #setLines(int)}.
4796 *
4797 * @return the minimum height of TextView in terms of pixels or -1 if the minimum height is not
4798 * defined in pixels
4799 *
4800 * @see #setMinHeight(int)
4801 * @see #setHeight(int)
4802 *
4803 * @attr ref android.R.styleable#TextView_minHeight
4804 */
4805 public int getMinHeight() {
4806 return mMinMode == PIXELS ? mMinimum : -1;
4807 }
4808
4809 /**
4810 * Sets the height of the TextView to be at most {@code maxLines} tall.
4811 * <p>
4812 * This value is used for height calculation if LayoutParams does not force TextView to have an
4813 * exact height. Setting this value overrides previous maximum height configurations such as
4814 * {@link #setMaxHeight(int)} or {@link #setLines(int)}.
4815 *
4816 * @param maxLines the maximum height of TextView in terms of number of lines
4817 *
4818 * @see #getMaxLines()
4819 * @see #setLines(int)
4820 *
4821 * @attr ref android.R.styleable#TextView_maxLines
4822 */
4823 @android.view.RemotableViewMethod
4824 public void setMaxLines(int maxLines) {
4825 mMaximum = maxLines;
4826 mMaxMode = LINES;
4827
4828 requestLayout();
4829 invalidate();
4830 }
4831
4832 /**
4833 * Returns the maximum height of TextView in terms of number of lines or -1 if the
4834 * maximum height was set using {@link #setMaxHeight(int)} or {@link #setHeight(int)}.
4835 *
4836 * @return the maximum height of TextView in terms of number of lines. -1 if the maximum height
4837 * is not defined in lines.
4838 *
4839 * @see #setMaxLines(int)
4840 * @see #setLines(int)
4841 *
4842 * @attr ref android.R.styleable#TextView_maxLines
4843 */
4844 public int getMaxLines() {
4845 return mMaxMode == LINES ? mMaximum : -1;
4846 }
4847
4848 /**
4849 * Sets the height of the TextView to be at most {@code maxPixels} tall.
4850 * <p>
4851 * This value is used for height calculation if LayoutParams does not force TextView to have an
4852 * exact height. Setting this value overrides previous maximum height configurations such as
4853 * {@link #setMaxLines(int)} or {@link #setLines(int)}.
4854 *
4855 * @param maxPixels the maximum height of TextView in terms of pixels
4856 *
4857 * @see #getMaxHeight()
4858 * @see #setHeight(int)
4859 *
4860 * @attr ref android.R.styleable#TextView_maxHeight
4861 */
4862 @android.view.RemotableViewMethod
4863 public void setMaxHeight(int maxPixels) {
4864 mMaximum = maxPixels;
4865 mMaxMode = PIXELS;
4866
4867 requestLayout();
4868 invalidate();
4869 }
4870
4871 /**
4872 * Returns the maximum height of TextView in terms of pixels or -1 if the maximum height was
4873 * set using {@link #setMaxLines(int)} or {@link #setLines(int)}.
4874 *
4875 * @return the maximum height of TextView in terms of pixels or -1 if the maximum height
4876 * is not defined in pixels
4877 *
4878 * @see #setMaxHeight(int)
4879 * @see #setHeight(int)
4880 *
4881 * @attr ref android.R.styleable#TextView_maxHeight
4882 */
4883 public int getMaxHeight() {
4884 return mMaxMode == PIXELS ? mMaximum : -1;
4885 }
4886
4887 /**
4888 * Sets the height of the TextView to be exactly {@code lines} tall.
4889 * <p>
4890 * This value is used for height calculation if LayoutParams does not force TextView to have an
4891 * exact height. Setting this value overrides previous minimum/maximum height configurations
4892 * such as {@link #setMinLines(int)} or {@link #setMaxLines(int)}. {@link #setSingleLine()} will
4893 * set this value to 1.
4894 *
4895 * @param lines the exact height of the TextView in terms of lines
4896 *
4897 * @see #setHeight(int)
4898 *
4899 * @attr ref android.R.styleable#TextView_lines
4900 */
4901 @android.view.RemotableViewMethod
4902 public void setLines(int lines) {
4903 mMaximum = mMinimum = lines;
4904 mMaxMode = mMinMode = LINES;
4905
4906 requestLayout();
4907 invalidate();
4908 }
4909
4910 /**
4911 * Sets the height of the TextView to be exactly <code>pixels</code> tall.
4912 * <p>
4913 * This value is used for height calculation if LayoutParams does not force TextView to have an
4914 * exact height. Setting this value overrides previous minimum/maximum height configurations
4915 * such as {@link #setMinHeight(int)} or {@link #setMaxHeight(int)}.
4916 *
4917 * @param pixels the exact height of the TextView in terms of pixels
4918 *
4919 * @see #setLines(int)
4920 *
4921 * @attr ref android.R.styleable#TextView_height
4922 */
4923 @android.view.RemotableViewMethod
4924 public void setHeight(int pixels) {
4925 mMaximum = mMinimum = pixels;
4926 mMaxMode = mMinMode = PIXELS;
4927
4928 requestLayout();
4929 invalidate();
4930 }
4931
4932 /**
4933 * Sets the width of the TextView to be at least {@code minEms} wide.
4934 * <p>
4935 * This value is used for width calculation if LayoutParams does not force TextView to have an
4936 * exact width. Setting this value overrides previous minimum width configurations such as
4937 * {@link #setMinWidth(int)} or {@link #setWidth(int)}.
4938 *
4939 * @param minEms the minimum width of TextView in terms of ems
4940 *
4941 * @see #getMinEms()
4942 * @see #setEms(int)
4943 *
4944 * @attr ref android.R.styleable#TextView_minEms
4945 */
4946 @android.view.RemotableViewMethod
4947 public void setMinEms(int minEms) {
4948 mMinWidth = minEms;
4949 mMinWidthMode = EMS;
4950
4951 requestLayout();
4952 invalidate();
4953 }
4954
4955 /**
4956 * Returns the minimum width of TextView in terms of ems or -1 if the minimum width was set
4957 * using {@link #setMinWidth(int)} or {@link #setWidth(int)}.
4958 *
4959 * @return the minimum width of TextView in terms of ems. -1 if the minimum width is not
4960 * defined in ems
4961 *
4962 * @see #setMinEms(int)
4963 * @see #setEms(int)
4964 *
4965 * @attr ref android.R.styleable#TextView_minEms
4966 */
4967 public int getMinEms() {
4968 return mMinWidthMode == EMS ? mMinWidth : -1;
4969 }
4970
4971 /**
4972 * Sets the width of the TextView to be at least {@code minPixels} wide.
4973 * <p>
4974 * This value is used for width calculation if LayoutParams does not force TextView to have an
4975 * exact width. Setting this value overrides previous minimum width configurations such as
4976 * {@link #setMinEms(int)} or {@link #setEms(int)}.
4977 * <p>
4978 * The value given here is different than {@link #setMinimumWidth(int)}. Between
4979 * {@code minWidth} and the value set in {@link #setMinimumWidth(int)}, the greater one is used
4980 * to decide the final width.
4981 *
4982 * @param minPixels the minimum width of TextView in terms of pixels
4983 *
4984 * @see #getMinWidth()
4985 * @see #setWidth(int)
4986 *
4987 * @attr ref android.R.styleable#TextView_minWidth
4988 */
4989 @android.view.RemotableViewMethod
4990 public void setMinWidth(int minPixels) {
4991 mMinWidth = minPixels;
4992 mMinWidthMode = PIXELS;
4993
4994 requestLayout();
4995 invalidate();
4996 }
4997
4998 /**
4999 * Returns the minimum width of TextView in terms of pixels or -1 if the minimum width was set
5000 * using {@link #setMinEms(int)} or {@link #setEms(int)}.
5001 *
5002 * @return the minimum width of TextView in terms of pixels or -1 if the minimum width is not
5003 * defined in pixels
5004 *
5005 * @see #setMinWidth(int)
5006 * @see #setWidth(int)
5007 *
5008 * @attr ref android.R.styleable#TextView_minWidth
5009 */
5010 public int getMinWidth() {
5011 return mMinWidthMode == PIXELS ? mMinWidth : -1;
5012 }
5013
5014 /**
5015 * Sets the width of the TextView to be at most {@code maxEms} wide.
5016 * <p>
5017 * This value is used for width calculation if LayoutParams does not force TextView to have an
5018 * exact width. Setting this value overrides previous maximum width configurations such as
5019 * {@link #setMaxWidth(int)} or {@link #setWidth(int)}.
5020 *
5021 * @param maxEms the maximum width of TextView in terms of ems
5022 *
5023 * @see #getMaxEms()
5024 * @see #setEms(int)
5025 *
5026 * @attr ref android.R.styleable#TextView_maxEms
5027 */
5028 @android.view.RemotableViewMethod
5029 public void setMaxEms(int maxEms) {
5030 mMaxWidth = maxEms;
5031 mMaxWidthMode = EMS;
5032
5033 requestLayout();
5034 invalidate();
5035 }
5036
5037 /**
5038 * Returns the maximum width of TextView in terms of ems or -1 if the maximum width was set
5039 * using {@link #setMaxWidth(int)} or {@link #setWidth(int)}.
5040 *
5041 * @return the maximum width of TextView in terms of ems or -1 if the maximum width is not
5042 * defined in ems
5043 *
5044 * @see #setMaxEms(int)
5045 * @see #setEms(int)
5046 *
5047 * @attr ref android.R.styleable#TextView_maxEms
5048 */
5049 public int getMaxEms() {
5050 return mMaxWidthMode == EMS ? mMaxWidth : -1;
5051 }
5052
5053 /**
5054 * Sets the width of the TextView to be at most {@code maxPixels} wide.
5055 * <p>
5056 * This value is used for width calculation if LayoutParams does not force TextView to have an
5057 * exact width. Setting this value overrides previous maximum width configurations such as
5058 * {@link #setMaxEms(int)} or {@link #setEms(int)}.
5059 *
5060 * @param maxPixels the maximum width of TextView in terms of pixels
5061 *
5062 * @see #getMaxWidth()
5063 * @see #setWidth(int)
5064 *
5065 * @attr ref android.R.styleable#TextView_maxWidth
5066 */
5067 @android.view.RemotableViewMethod
5068 public void setMaxWidth(int maxPixels) {
5069 mMaxWidth = maxPixels;
5070 mMaxWidthMode = PIXELS;
5071
5072 requestLayout();
5073 invalidate();
5074 }
5075
5076 /**
5077 * Returns the maximum width of TextView in terms of pixels or -1 if the maximum width was set
5078 * using {@link #setMaxEms(int)} or {@link #setEms(int)}.
5079 *
5080 * @return the maximum width of TextView in terms of pixels. -1 if the maximum width is not
5081 * defined in pixels
5082 *
5083 * @see #setMaxWidth(int)
5084 * @see #setWidth(int)
5085 *
5086 * @attr ref android.R.styleable#TextView_maxWidth
5087 */
5088 public int getMaxWidth() {
5089 return mMaxWidthMode == PIXELS ? mMaxWidth : -1;
5090 }
5091
5092 /**
5093 * Sets the width of the TextView to be exactly {@code ems} wide.
5094 *
5095 * This value is used for width calculation if LayoutParams does not force TextView to have an
5096 * exact width. Setting this value overrides previous minimum/maximum configurations such as
5097 * {@link #setMinEms(int)} or {@link #setMaxEms(int)}.
5098 *
5099 * @param ems the exact width of the TextView in terms of ems
5100 *
5101 * @see #setWidth(int)
5102 *
5103 * @attr ref android.R.styleable#TextView_ems
5104 */
5105 @android.view.RemotableViewMethod
5106 public void setEms(int ems) {
5107 mMaxWidth = mMinWidth = ems;
5108 mMaxWidthMode = mMinWidthMode = EMS;
5109
5110 requestLayout();
5111 invalidate();
5112 }
5113
5114 /**
5115 * Sets the width of the TextView to be exactly {@code pixels} wide.
5116 * <p>
5117 * This value is used for width calculation if LayoutParams does not force TextView to have an
5118 * exact width. Setting this value overrides previous minimum/maximum width configurations
5119 * such as {@link #setMinWidth(int)} or {@link #setMaxWidth(int)}.
5120 *
5121 * @param pixels the exact width of the TextView in terms of pixels
5122 *
5123 * @see #setEms(int)
5124 *
5125 * @attr ref android.R.styleable#TextView_width
5126 */
5127 @android.view.RemotableViewMethod
5128 public void setWidth(int pixels) {
5129 mMaxWidth = mMinWidth = pixels;
5130 mMaxWidthMode = mMinWidthMode = PIXELS;
5131
5132 requestLayout();
5133 invalidate();
5134 }
5135
5136 /**
5137 * Sets line spacing for this TextView. Each line other than the last line will have its height
5138 * multiplied by {@code mult} and have {@code add} added to it.
5139 *
Justin Klaassen98fe7812018-01-03 13:39:41 -05005140 * @param add The value in pixels that should be added to each line other than the last line.
5141 * This will be applied after the multiplier
5142 * @param mult The value by which each line height other than the last line will be multiplied
5143 * by
Justin Klaassen10d07c82017-09-15 17:58:39 -04005144 *
5145 * @attr ref android.R.styleable#TextView_lineSpacingExtra
5146 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
5147 */
5148 public void setLineSpacing(float add, float mult) {
5149 if (mSpacingAdd != add || mSpacingMult != mult) {
5150 mSpacingAdd = add;
5151 mSpacingMult = mult;
5152
5153 if (mLayout != null) {
5154 nullLayouts();
5155 requestLayout();
5156 invalidate();
5157 }
5158 }
5159 }
5160
5161 /**
5162 * Gets the line spacing multiplier
5163 *
5164 * @return the value by which each line's height is multiplied to get its actual height.
5165 *
5166 * @see #setLineSpacing(float, float)
5167 * @see #getLineSpacingExtra()
5168 *
5169 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
5170 */
5171 public float getLineSpacingMultiplier() {
5172 return mSpacingMult;
5173 }
5174
5175 /**
5176 * Gets the line spacing extra space
5177 *
5178 * @return the extra space that is added to the height of each lines of this TextView.
5179 *
5180 * @see #setLineSpacing(float, float)
5181 * @see #getLineSpacingMultiplier()
5182 *
5183 * @attr ref android.R.styleable#TextView_lineSpacingExtra
5184 */
5185 public float getLineSpacingExtra() {
5186 return mSpacingAdd;
5187 }
5188
5189 /**
Jeff Davidsona192cc22018-02-08 15:30:06 -08005190 * Sets an explicit line height for this TextView. This is equivalent to the vertical distance
5191 * between subsequent baselines in the TextView.
5192 *
5193 * @param lineHeight the line height in pixels
5194 *
5195 * @see #setLineSpacing(float, float)
5196 * @see #getLineSpacing()
5197 *
5198 * @attr ref android.R.styleable#TextView_lineHeight
5199 */
5200 public void setLineHeight(@Px @IntRange(from = 0) int lineHeight) {
5201 Preconditions.checkArgumentNonnegative(lineHeight);
5202
5203 final int fontHeight = getPaint().getFontMetricsInt(null);
5204 // Make sure we don't setLineSpacing if it's not needed to avoid unnecessary redraw.
5205 if (lineHeight != fontHeight) {
5206 // Set lineSpacingExtra by the difference of lineSpacing with lineHeight
5207 setLineSpacing(lineHeight - fontHeight, 1f);
5208 }
5209 }
5210
5211 /**
Justin Klaassen10d07c82017-09-15 17:58:39 -04005212 * Convenience method to append the specified text to the TextView's
5213 * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
5214 * if it was not already editable.
5215 *
5216 * @param text text to be appended to the already displayed text
5217 */
5218 public final void append(CharSequence text) {
5219 append(text, 0, text.length());
5220 }
5221
5222 /**
5223 * Convenience method to append the specified text slice to the TextView's
5224 * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
5225 * if it was not already editable.
5226 *
5227 * @param text text to be appended to the already displayed text
5228 * @param start the index of the first character in the {@code text}
5229 * @param end the index of the character following the last character in the {@code text}
5230 *
5231 * @see Appendable#append(CharSequence, int, int)
5232 */
5233 public void append(CharSequence text, int start, int end) {
5234 if (!(mText instanceof Editable)) {
5235 setText(mText, BufferType.EDITABLE);
5236 }
5237
5238 ((Editable) mText).append(text, start, end);
5239
5240 if (mAutoLinkMask != 0) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005241 boolean linksWereAdded = Linkify.addLinks(mSpannable, mAutoLinkMask);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005242 // Do not change the movement method for text that support text selection as it
5243 // would prevent an arbitrary cursor displacement.
5244 if (linksWereAdded && mLinksClickable && !textCanBeSelected()) {
5245 setMovementMethod(LinkMovementMethod.getInstance());
5246 }
5247 }
5248 }
5249
5250 private void updateTextColors() {
5251 boolean inval = false;
5252 final int[] drawableState = getDrawableState();
5253 int color = mTextColor.getColorForState(drawableState, 0);
5254 if (color != mCurTextColor) {
5255 mCurTextColor = color;
5256 inval = true;
5257 }
5258 if (mLinkTextColor != null) {
5259 color = mLinkTextColor.getColorForState(drawableState, 0);
5260 if (color != mTextPaint.linkColor) {
5261 mTextPaint.linkColor = color;
5262 inval = true;
5263 }
5264 }
5265 if (mHintTextColor != null) {
5266 color = mHintTextColor.getColorForState(drawableState, 0);
5267 if (color != mCurHintTextColor) {
5268 mCurHintTextColor = color;
5269 if (mText.length() == 0) {
5270 inval = true;
5271 }
5272 }
5273 }
5274 if (inval) {
5275 // Text needs to be redrawn with the new color
5276 if (mEditor != null) mEditor.invalidateTextDisplayList();
5277 invalidate();
5278 }
5279 }
5280
5281 @Override
5282 protected void drawableStateChanged() {
5283 super.drawableStateChanged();
5284
5285 if (mTextColor != null && mTextColor.isStateful()
5286 || (mHintTextColor != null && mHintTextColor.isStateful())
5287 || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
5288 updateTextColors();
5289 }
5290
5291 if (mDrawables != null) {
5292 final int[] state = getDrawableState();
5293 for (Drawable dr : mDrawables.mShowing) {
5294 if (dr != null && dr.isStateful() && dr.setState(state)) {
5295 invalidateDrawable(dr);
5296 }
5297 }
5298 }
5299 }
5300
5301 @Override
5302 public void drawableHotspotChanged(float x, float y) {
5303 super.drawableHotspotChanged(x, y);
5304
5305 if (mDrawables != null) {
5306 for (Drawable dr : mDrawables.mShowing) {
5307 if (dr != null) {
5308 dr.setHotspot(x, y);
5309 }
5310 }
5311 }
5312 }
5313
5314 @Override
5315 public Parcelable onSaveInstanceState() {
5316 Parcelable superState = super.onSaveInstanceState();
5317
5318 // Save state if we are forced to
5319 final boolean freezesText = getFreezesText();
5320 boolean hasSelection = false;
5321 int start = -1;
5322 int end = -1;
5323
5324 if (mText != null) {
5325 start = getSelectionStart();
5326 end = getSelectionEnd();
5327 if (start >= 0 || end >= 0) {
5328 // Or save state if there is a selection
5329 hasSelection = true;
5330 }
5331 }
5332
5333 if (freezesText || hasSelection) {
5334 SavedState ss = new SavedState(superState);
5335
5336 if (freezesText) {
5337 if (mText instanceof Spanned) {
5338 final Spannable sp = new SpannableStringBuilder(mText);
5339
5340 if (mEditor != null) {
5341 removeMisspelledSpans(sp);
5342 sp.removeSpan(mEditor.mSuggestionRangeSpan);
5343 }
5344
5345 ss.text = sp;
5346 } else {
5347 ss.text = mText.toString();
5348 }
5349 }
5350
5351 if (hasSelection) {
5352 // XXX Should also save the current scroll position!
5353 ss.selStart = start;
5354 ss.selEnd = end;
5355 }
5356
5357 if (isFocused() && start >= 0 && end >= 0) {
5358 ss.frozenWithFocus = true;
5359 }
5360
5361 ss.error = getError();
5362
5363 if (mEditor != null) {
5364 ss.editorState = mEditor.saveInstanceState();
5365 }
5366 return ss;
5367 }
5368
5369 return superState;
5370 }
5371
5372 void removeMisspelledSpans(Spannable spannable) {
5373 SuggestionSpan[] suggestionSpans = spannable.getSpans(0, spannable.length(),
5374 SuggestionSpan.class);
5375 for (int i = 0; i < suggestionSpans.length; i++) {
5376 int flags = suggestionSpans[i].getFlags();
5377 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
5378 && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
5379 spannable.removeSpan(suggestionSpans[i]);
5380 }
5381 }
5382 }
5383
5384 @Override
5385 public void onRestoreInstanceState(Parcelable state) {
5386 if (!(state instanceof SavedState)) {
5387 super.onRestoreInstanceState(state);
5388 return;
5389 }
5390
5391 SavedState ss = (SavedState) state;
5392 super.onRestoreInstanceState(ss.getSuperState());
5393
5394 // XXX restore buffer type too, as well as lots of other stuff
5395 if (ss.text != null) {
5396 setText(ss.text);
5397 }
5398
5399 if (ss.selStart >= 0 && ss.selEnd >= 0) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005400 if (mSpannable != null) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04005401 int len = mText.length();
5402
5403 if (ss.selStart > len || ss.selEnd > len) {
5404 String restored = "";
5405
5406 if (ss.text != null) {
5407 restored = "(restored) ";
5408 }
5409
5410 Log.e(LOG_TAG, "Saved cursor position " + ss.selStart + "/" + ss.selEnd
5411 + " out of range for " + restored + "text " + mText);
5412 } else {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005413 Selection.setSelection(mSpannable, ss.selStart, ss.selEnd);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005414
5415 if (ss.frozenWithFocus) {
5416 createEditorIfNeeded();
5417 mEditor.mFrozenWithFocus = true;
5418 }
5419 }
5420 }
5421 }
5422
5423 if (ss.error != null) {
5424 final CharSequence error = ss.error;
5425 // Display the error later, after the first layout pass
5426 post(new Runnable() {
5427 public void run() {
5428 if (mEditor == null || !mEditor.mErrorWasChanged) {
5429 setError(error);
5430 }
5431 }
5432 });
5433 }
5434
5435 if (ss.editorState != null) {
5436 createEditorIfNeeded();
5437 mEditor.restoreInstanceState(ss.editorState);
5438 }
5439 }
5440
5441 /**
5442 * Control whether this text view saves its entire text contents when
5443 * freezing to an icicle, in addition to dynamic state such as cursor
5444 * position. By default this is false, not saving the text. Set to true
5445 * if the text in the text view is not being saved somewhere else in
5446 * persistent storage (such as in a content provider) so that if the
5447 * view is later thawed the user will not lose their data. For
5448 * {@link android.widget.EditText} it is always enabled, regardless of
5449 * the value of the attribute.
5450 *
5451 * @param freezesText Controls whether a frozen icicle should include the
5452 * entire text data: true to include it, false to not.
5453 *
5454 * @attr ref android.R.styleable#TextView_freezesText
5455 */
5456 @android.view.RemotableViewMethod
5457 public void setFreezesText(boolean freezesText) {
5458 mFreezesText = freezesText;
5459 }
5460
5461 /**
5462 * Return whether this text view is including its entire text contents
5463 * in frozen icicles. For {@link android.widget.EditText} it always returns true.
5464 *
5465 * @return Returns true if text is included, false if it isn't.
5466 *
5467 * @see #setFreezesText
5468 */
5469 public boolean getFreezesText() {
5470 return mFreezesText;
5471 }
5472
5473 ///////////////////////////////////////////////////////////////////////////
5474
5475 /**
5476 * Sets the Factory used to create new {@link Editable Editables}.
5477 *
5478 * @param factory {@link android.text.Editable.Factory Editable.Factory} to be used
5479 *
5480 * @see android.text.Editable.Factory
5481 * @see android.widget.TextView.BufferType#EDITABLE
5482 */
5483 public final void setEditableFactory(Editable.Factory factory) {
5484 mEditableFactory = factory;
5485 setText(mText);
5486 }
5487
5488 /**
5489 * Sets the Factory used to create new {@link Spannable Spannables}.
5490 *
5491 * @param factory {@link android.text.Spannable.Factory Spannable.Factory} to be used
5492 *
5493 * @see android.text.Spannable.Factory
5494 * @see android.widget.TextView.BufferType#SPANNABLE
5495 */
5496 public final void setSpannableFactory(Spannable.Factory factory) {
5497 mSpannableFactory = factory;
5498 setText(mText);
5499 }
5500
5501 /**
5502 * Sets the text to be displayed. TextView <em>does not</em> accept
5503 * HTML-like formatting, which you can do with text strings in XML resource files.
5504 * To style your strings, attach android.text.style.* objects to a
5505 * {@link android.text.SpannableString}, or see the
5506 * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
5507 * Available Resource Types</a> documentation for an example of setting
5508 * formatted text in the XML resource file.
5509 * <p/>
5510 * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
5511 * intermediate {@link Spannable Spannables}. Likewise it will use
5512 * {@link android.text.Editable.Factory} to create final or intermediate
5513 * {@link Editable Editables}.
5514 *
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005515 * If the passed text is a {@link PrecomputedText} but the parameters used to create the
5516 * PrecomputedText mismatches with this TextView, IllegalArgumentException is thrown. To ensure
5517 * the parameters match, you can call {@link TextView#setTextMetricsParams} before calling this.
5518 *
Justin Klaassen10d07c82017-09-15 17:58:39 -04005519 * @param text text to be displayed
5520 *
5521 * @attr ref android.R.styleable#TextView_text
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005522 * @throws IllegalArgumentException if the passed text is a {@link PrecomputedText} but the
5523 * parameters used to create the PrecomputedText mismatches
5524 * with this TextView.
Justin Klaassen10d07c82017-09-15 17:58:39 -04005525 */
5526 @android.view.RemotableViewMethod
5527 public final void setText(CharSequence text) {
5528 setText(text, mBufferType);
5529 }
5530
5531 /**
5532 * Sets the text to be displayed but retains the cursor position. Same as
5533 * {@link #setText(CharSequence)} except that the cursor position (if any) is retained in the
5534 * new text.
5535 * <p/>
5536 * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
5537 * intermediate {@link Spannable Spannables}. Likewise it will use
5538 * {@link android.text.Editable.Factory} to create final or intermediate
5539 * {@link Editable Editables}.
5540 *
5541 * @param text text to be displayed
5542 *
5543 * @see #setText(CharSequence)
5544 */
5545 @android.view.RemotableViewMethod
5546 public final void setTextKeepState(CharSequence text) {
5547 setTextKeepState(text, mBufferType);
5548 }
5549
5550 /**
5551 * Sets the text to be displayed and the {@link android.widget.TextView.BufferType}.
5552 * <p/>
5553 * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
5554 * intermediate {@link Spannable Spannables}. Likewise it will use
5555 * {@link android.text.Editable.Factory} to create final or intermediate
5556 * {@link Editable Editables}.
5557 *
5558 * @param text text to be displayed
5559 * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
5560 * stored as a static text, styleable/spannable text, or editable text
5561 *
5562 * @see #setText(CharSequence)
5563 * @see android.widget.TextView.BufferType
5564 * @see #setSpannableFactory(Spannable.Factory)
5565 * @see #setEditableFactory(Editable.Factory)
5566 *
5567 * @attr ref android.R.styleable#TextView_text
5568 * @attr ref android.R.styleable#TextView_bufferType
5569 */
5570 public void setText(CharSequence text, BufferType type) {
5571 setText(text, type, true, 0);
5572
5573 if (mCharWrapper != null) {
5574 mCharWrapper.mChars = null;
5575 }
5576 }
5577
5578 private void setText(CharSequence text, BufferType type,
5579 boolean notifyBefore, int oldlen) {
Jeff Davidsona192cc22018-02-08 15:30:06 -08005580 mTextSetFromXmlOrResourceId = false;
Justin Klaassen10d07c82017-09-15 17:58:39 -04005581 if (text == null) {
5582 text = "";
5583 }
5584
5585 // If suggestions are not enabled, remove the suggestion spans from the text
5586 if (!isSuggestionsEnabled()) {
5587 text = removeSuggestionSpans(text);
5588 }
5589
5590 if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
5591
5592 if (text instanceof Spanned
5593 && ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
5594 if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
5595 setHorizontalFadingEdgeEnabled(true);
5596 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
5597 } else {
5598 setHorizontalFadingEdgeEnabled(false);
5599 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
5600 }
5601 setEllipsize(TextUtils.TruncateAt.MARQUEE);
5602 }
5603
5604 int n = mFilters.length;
5605 for (int i = 0; i < n; i++) {
5606 CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
5607 if (out != null) {
5608 text = out;
5609 }
5610 }
5611
5612 if (notifyBefore) {
5613 if (mText != null) {
5614 oldlen = mText.length();
5615 sendBeforeTextChanged(mText, 0, oldlen, text.length());
5616 } else {
5617 sendBeforeTextChanged("", 0, 0, text.length());
5618 }
5619 }
5620
5621 boolean needEditableForNotification = false;
5622
5623 if (mListeners != null && mListeners.size() != 0) {
5624 needEditableForNotification = true;
5625 }
5626
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005627 PrecomputedText precomputed =
5628 (text instanceof PrecomputedText) ? (PrecomputedText) text : null;
Justin Klaassen10d07c82017-09-15 17:58:39 -04005629 if (type == BufferType.EDITABLE || getKeyListener() != null
5630 || needEditableForNotification) {
5631 createEditorIfNeeded();
5632 mEditor.forgetUndoRedo();
5633 Editable t = mEditableFactory.newEditable(text);
5634 text = t;
5635 setFilters(t, mFilters);
5636 InputMethodManager imm = InputMethodManager.peekInstance();
5637 if (imm != null) imm.restartInput(this);
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005638 } else if (precomputed != null) {
5639 if (mTextDir == null) {
5640 mTextDir = getTextDirectionHeuristic();
5641 }
5642 if (!precomputed.getParams().isSameTextMetricsInternal(
5643 getPaint(), mTextDir, mBreakStrategy, mHyphenationFrequency)) {
5644 throw new IllegalArgumentException(
5645 "PrecomputedText's Parameters don't match the parameters of this TextView."
5646 + "Consider using setTextMetricsParams(precomputedText.getParams()) "
5647 + "to override the settings of this TextView: "
5648 + "PrecomputedText: " + precomputed.getParams()
5649 + "TextView: " + getTextMetricsParams());
5650 }
Justin Klaassen10d07c82017-09-15 17:58:39 -04005651 } else if (type == BufferType.SPANNABLE || mMovement != null) {
5652 text = mSpannableFactory.newSpannable(text);
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005653 } else if (!(text instanceof CharWrapper)) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04005654 text = TextUtils.stringOrSpannedString(text);
5655 }
5656
5657 if (mAutoLinkMask != 0) {
5658 Spannable s2;
5659
5660 if (type == BufferType.EDITABLE || text instanceof Spannable) {
5661 s2 = (Spannable) text;
5662 } else {
5663 s2 = mSpannableFactory.newSpannable(text);
5664 }
5665
5666 if (Linkify.addLinks(s2, mAutoLinkMask)) {
5667 text = s2;
5668 type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
5669
5670 /*
5671 * We must go ahead and set the text before changing the
5672 * movement method, because setMovementMethod() may call
5673 * setText() again to try to upgrade the buffer type.
5674 */
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005675 setTextInternal(text);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005676
5677 // Do not change the movement method for text that support text selection as it
5678 // would prevent an arbitrary cursor displacement.
5679 if (mLinksClickable && !textCanBeSelected()) {
5680 setMovementMethod(LinkMovementMethod.getInstance());
5681 }
5682 }
5683 }
5684
5685 mBufferType = type;
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005686 setTextInternal(text);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005687
5688 if (mTransformation == null) {
5689 mTransformed = text;
5690 } else {
5691 mTransformed = mTransformation.getTransformation(text, this);
5692 }
5693
5694 final int textLength = text.length();
5695
5696 if (text instanceof Spannable && !mAllowTransformationLengthChange) {
5697 Spannable sp = (Spannable) text;
5698
5699 // Remove any ChangeWatchers that might have come from other TextViews.
5700 final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
5701 final int count = watchers.length;
5702 for (int i = 0; i < count; i++) {
5703 sp.removeSpan(watchers[i]);
5704 }
5705
5706 if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();
5707
5708 sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
5709 | (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
5710
5711 if (mEditor != null) mEditor.addSpanWatchers(sp);
5712
5713 if (mTransformation != null) {
5714 sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
5715 }
5716
5717 if (mMovement != null) {
5718 mMovement.initialize(this, (Spannable) text);
5719
5720 /*
5721 * Initializing the movement method will have set the
5722 * selection, so reset mSelectionMoved to keep that from
5723 * interfering with the normal on-focus selection-setting.
5724 */
5725 if (mEditor != null) mEditor.mSelectionMoved = false;
5726 }
5727 }
5728
5729 if (mLayout != null) {
5730 checkForRelayout();
5731 }
5732
5733 sendOnTextChanged(text, 0, oldlen, textLength);
5734 onTextChanged(text, 0, oldlen, textLength);
5735
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005736 notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005737
5738 if (needEditableForNotification) {
5739 sendAfterTextChanged((Editable) text);
5740 } else {
Justin Klaassen10d07c82017-09-15 17:58:39 -04005741 notifyAutoFillManagerAfterTextChangedIfNeeded();
5742 }
5743
5744 // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
5745 if (mEditor != null) mEditor.prepareCursorControllers();
5746 }
5747
5748 /**
5749 * Sets the TextView to display the specified slice of the specified
5750 * char array. You must promise that you will not change the contents
5751 * of the array except for right before another call to setText(),
5752 * since the TextView has no way to know that the text
5753 * has changed and that it needs to invalidate and re-layout.
5754 *
5755 * @param text char array to be displayed
5756 * @param start start index in the char array
5757 * @param len length of char count after {@code start}
5758 */
5759 public final void setText(char[] text, int start, int len) {
5760 int oldlen = 0;
5761
5762 if (start < 0 || len < 0 || start + len > text.length) {
5763 throw new IndexOutOfBoundsException(start + ", " + len);
5764 }
5765
5766 /*
5767 * We must do the before-notification here ourselves because if
5768 * the old text is a CharWrapper we destroy it before calling
5769 * into the normal path.
5770 */
5771 if (mText != null) {
5772 oldlen = mText.length();
5773 sendBeforeTextChanged(mText, 0, oldlen, len);
5774 } else {
5775 sendBeforeTextChanged("", 0, 0, len);
5776 }
5777
5778 if (mCharWrapper == null) {
5779 mCharWrapper = new CharWrapper(text, start, len);
5780 } else {
5781 mCharWrapper.set(text, start, len);
5782 }
5783
5784 setText(mCharWrapper, mBufferType, false, oldlen);
5785 }
5786
5787 /**
5788 * Sets the text to be displayed and the {@link android.widget.TextView.BufferType} but retains
5789 * the cursor position. Same as
5790 * {@link #setText(CharSequence, android.widget.TextView.BufferType)} except that the cursor
5791 * position (if any) is retained in the new text.
5792 * <p/>
5793 * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
5794 * intermediate {@link Spannable Spannables}. Likewise it will use
5795 * {@link android.text.Editable.Factory} to create final or intermediate
5796 * {@link Editable Editables}.
5797 *
5798 * @param text text to be displayed
5799 * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
5800 * stored as a static text, styleable/spannable text, or editable text
5801 *
5802 * @see #setText(CharSequence, android.widget.TextView.BufferType)
5803 */
5804 public final void setTextKeepState(CharSequence text, BufferType type) {
5805 int start = getSelectionStart();
5806 int end = getSelectionEnd();
5807 int len = text.length();
5808
5809 setText(text, type);
5810
5811 if (start >= 0 || end >= 0) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005812 if (mSpannable != null) {
5813 Selection.setSelection(mSpannable,
Justin Klaassen10d07c82017-09-15 17:58:39 -04005814 Math.max(0, Math.min(start, len)),
5815 Math.max(0, Math.min(end, len)));
5816 }
5817 }
5818 }
5819
5820 /**
5821 * Sets the text to be displayed using a string resource identifier.
5822 *
5823 * @param resid the resource identifier of the string resource to be displayed
5824 *
5825 * @see #setText(CharSequence)
5826 *
5827 * @attr ref android.R.styleable#TextView_text
5828 */
5829 @android.view.RemotableViewMethod
5830 public final void setText(@StringRes int resid) {
5831 setText(getContext().getResources().getText(resid));
Jeff Davidsona192cc22018-02-08 15:30:06 -08005832 mTextSetFromXmlOrResourceId = true;
5833 mTextId = resid;
Justin Klaassen10d07c82017-09-15 17:58:39 -04005834 }
5835
5836 /**
5837 * Sets the text to be displayed using a string resource identifier and the
5838 * {@link android.widget.TextView.BufferType}.
5839 * <p/>
5840 * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
5841 * intermediate {@link Spannable Spannables}. Likewise it will use
5842 * {@link android.text.Editable.Factory} to create final or intermediate
5843 * {@link Editable Editables}.
5844 *
5845 * @param resid the resource identifier of the string resource to be displayed
5846 * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
5847 * stored as a static text, styleable/spannable text, or editable text
5848 *
5849 * @see #setText(int)
5850 * @see #setText(CharSequence)
5851 * @see android.widget.TextView.BufferType
5852 * @see #setSpannableFactory(Spannable.Factory)
5853 * @see #setEditableFactory(Editable.Factory)
5854 *
5855 * @attr ref android.R.styleable#TextView_text
5856 * @attr ref android.R.styleable#TextView_bufferType
5857 */
5858 public final void setText(@StringRes int resid, BufferType type) {
5859 setText(getContext().getResources().getText(resid), type);
Jeff Davidsona192cc22018-02-08 15:30:06 -08005860 mTextSetFromXmlOrResourceId = true;
5861 mTextId = resid;
Justin Klaassen10d07c82017-09-15 17:58:39 -04005862 }
5863
5864 /**
5865 * Sets the text to be displayed when the text of the TextView is empty.
5866 * Null means to use the normal empty text. The hint does not currently
5867 * participate in determining the size of the view.
5868 *
5869 * @attr ref android.R.styleable#TextView_hint
5870 */
5871 @android.view.RemotableViewMethod
5872 public final void setHint(CharSequence hint) {
Justin Klaassenbc81c7a2017-09-18 17:38:50 -04005873 setHintInternal(hint);
5874
Justin Klaassen93b7ee42017-10-10 15:20:13 -04005875 if (mEditor != null && isInputMethodTarget()) {
Justin Klaassenbc81c7a2017-09-18 17:38:50 -04005876 mEditor.reportExtractedText();
5877 }
5878 }
5879
5880 private void setHintInternal(CharSequence hint) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04005881 mHint = TextUtils.stringOrSpannedString(hint);
5882
5883 if (mLayout != null) {
5884 checkForRelayout();
5885 }
5886
5887 if (mText.length() == 0) {
5888 invalidate();
5889 }
5890
5891 // Invalidate display list if hint is currently used
5892 if (mEditor != null && mText.length() == 0 && mHint != null) {
5893 mEditor.invalidateTextDisplayList();
5894 }
5895 }
5896
5897 /**
5898 * Sets the text to be displayed when the text of the TextView is empty,
5899 * from a resource.
5900 *
5901 * @attr ref android.R.styleable#TextView_hint
5902 */
5903 @android.view.RemotableViewMethod
5904 public final void setHint(@StringRes int resid) {
5905 setHint(getContext().getResources().getText(resid));
5906 }
5907
5908 /**
5909 * Returns the hint that is displayed when the text of the TextView
5910 * is empty.
5911 *
5912 * @attr ref android.R.styleable#TextView_hint
5913 */
5914 @ViewDebug.CapturedViewProperty
5915 public CharSequence getHint() {
5916 return mHint;
5917 }
5918
5919 boolean isSingleLine() {
5920 return mSingleLine;
5921 }
5922
5923 private static boolean isMultilineInputType(int type) {
5924 return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE))
5925 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
5926 }
5927
5928 /**
5929 * Removes the suggestion spans.
5930 */
5931 CharSequence removeSuggestionSpans(CharSequence text) {
5932 if (text instanceof Spanned) {
5933 Spannable spannable;
5934 if (text instanceof Spannable) {
5935 spannable = (Spannable) text;
5936 } else {
5937 spannable = mSpannableFactory.newSpannable(text);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005938 }
5939
5940 SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
Justin Klaassen98fe7812018-01-03 13:39:41 -05005941 if (spans.length == 0) {
5942 return text;
5943 } else {
5944 text = spannable;
5945 }
5946
Justin Klaassen10d07c82017-09-15 17:58:39 -04005947 for (int i = 0; i < spans.length; i++) {
5948 spannable.removeSpan(spans[i]);
5949 }
5950 }
5951 return text;
5952 }
5953
5954 /**
5955 * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
5956 * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
5957 * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
5958 * then a soft keyboard will not be displayed for this text view.
5959 *
5960 * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
5961 * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
5962 * type.
5963 *
5964 * @see #getInputType()
5965 * @see #setRawInputType(int)
5966 * @see android.text.InputType
5967 * @attr ref android.R.styleable#TextView_inputType
5968 */
5969 public void setInputType(int type) {
5970 final boolean wasPassword = isPasswordInputType(getInputType());
5971 final boolean wasVisiblePassword = isVisiblePasswordInputType(getInputType());
5972 setInputType(type, false);
5973 final boolean isPassword = isPasswordInputType(type);
5974 final boolean isVisiblePassword = isVisiblePasswordInputType(type);
5975 boolean forceUpdate = false;
5976 if (isPassword) {
5977 setTransformationMethod(PasswordTransformationMethod.getInstance());
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005978 setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE,
5979 Typeface.NORMAL, -1 /* weight, not specifeid */);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005980 } else if (isVisiblePassword) {
5981 if (mTransformation == PasswordTransformationMethod.getInstance()) {
5982 forceUpdate = true;
5983 }
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005984 setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE,
5985 Typeface.NORMAL, -1 /* weight, not specified */);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005986 } else if (wasPassword || wasVisiblePassword) {
5987 // not in password mode, clean up typeface and transformation
Justin Klaassen4d01eea2018-04-03 23:21:57 -04005988 setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */,
5989 DEFAULT_TYPEFACE /* typeface index */, Typeface.NORMAL,
5990 -1 /* weight, not specified */);
Justin Klaassen10d07c82017-09-15 17:58:39 -04005991 if (mTransformation == PasswordTransformationMethod.getInstance()) {
5992 forceUpdate = true;
5993 }
5994 }
5995
5996 boolean singleLine = !isMultilineInputType(type);
5997
5998 // We need to update the single line mode if it has changed or we
5999 // were previously in password mode.
6000 if (mSingleLine != singleLine || forceUpdate) {
6001 // Change single line mode, but only change the transformation if
6002 // we are not in password mode.
6003 applySingleLine(singleLine, !isPassword, true);
6004 }
6005
6006 if (!isSuggestionsEnabled()) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04006007 setTextInternal(removeSuggestionSpans(mText));
Justin Klaassen10d07c82017-09-15 17:58:39 -04006008 }
6009
6010 InputMethodManager imm = InputMethodManager.peekInstance();
6011 if (imm != null) imm.restartInput(this);
6012 }
6013
6014 /**
6015 * It would be better to rely on the input type for everything. A password inputType should have
6016 * a password transformation. We should hence use isPasswordInputType instead of this method.
6017 *
6018 * We should:
6019 * - Call setInputType in setKeyListener instead of changing the input type directly (which
6020 * would install the correct transformation).
6021 * - Refuse the installation of a non-password transformation in setTransformation if the input
6022 * type is password.
6023 *
6024 * However, this is like this for legacy reasons and we cannot break existing apps. This method
6025 * is useful since it matches what the user can see (obfuscated text or not).
6026 *
6027 * @return true if the current transformation method is of the password type.
6028 */
6029 boolean hasPasswordTransformationMethod() {
6030 return mTransformation instanceof PasswordTransformationMethod;
6031 }
6032
6033 static boolean isPasswordInputType(int inputType) {
6034 final int variation =
6035 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
6036 return variation
6037 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
6038 || variation
6039 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
6040 || variation
6041 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
6042 }
6043
6044 private static boolean isVisiblePasswordInputType(int inputType) {
6045 final int variation =
6046 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
6047 return variation
6048 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
6049 }
6050
6051 /**
6052 * Directly change the content type integer of the text view, without
6053 * modifying any other state.
6054 * @see #setInputType(int)
6055 * @see android.text.InputType
6056 * @attr ref android.R.styleable#TextView_inputType
6057 */
6058 public void setRawInputType(int type) {
6059 if (type == InputType.TYPE_NULL && mEditor == null) return; //TYPE_NULL is the default value
6060 createEditorIfNeeded();
6061 mEditor.mInputType = type;
6062 }
6063
6064 /**
6065 * @return {@code null} if the key listener should use pre-O (locale-independent). Otherwise
6066 * a {@code Locale} object that can be used to customize key various listeners.
6067 * @see DateKeyListener#getInstance(Locale)
6068 * @see DateTimeKeyListener#getInstance(Locale)
6069 * @see DigitsKeyListener#getInstance(Locale)
6070 * @see TimeKeyListener#getInstance(Locale)
6071 */
6072 @Nullable
6073 private Locale getCustomLocaleForKeyListenerOrNull() {
6074 if (!mUseInternationalizedInput) {
6075 // If the application does not target O, stick to the previous behavior.
6076 return null;
6077 }
6078 final LocaleList locales = getImeHintLocales();
6079 if (locales == null) {
6080 // If the application does not explicitly specify IME hint locale, also stick to the
6081 // previous behavior.
6082 return null;
6083 }
6084 return locales.get(0);
6085 }
6086
6087 private void setInputType(int type, boolean direct) {
6088 final int cls = type & EditorInfo.TYPE_MASK_CLASS;
6089 KeyListener input;
6090 if (cls == EditorInfo.TYPE_CLASS_TEXT) {
6091 boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
6092 TextKeyListener.Capitalize cap;
6093 if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
6094 cap = TextKeyListener.Capitalize.CHARACTERS;
6095 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
6096 cap = TextKeyListener.Capitalize.WORDS;
6097 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
6098 cap = TextKeyListener.Capitalize.SENTENCES;
6099 } else {
6100 cap = TextKeyListener.Capitalize.NONE;
6101 }
6102 input = TextKeyListener.getInstance(autotext, cap);
6103 } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
6104 final Locale locale = getCustomLocaleForKeyListenerOrNull();
6105 input = DigitsKeyListener.getInstance(
6106 locale,
6107 (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
6108 (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
6109 if (locale != null) {
6110 // Override type, if necessary for i18n.
6111 int newType = input.getInputType();
6112 final int newClass = newType & EditorInfo.TYPE_MASK_CLASS;
6113 if (newClass != EditorInfo.TYPE_CLASS_NUMBER) {
6114 // The class is different from the original class. So we need to override
6115 // 'type'. But we want to keep the password flag if it's there.
6116 if ((type & EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD) != 0) {
6117 newType |= EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
6118 }
6119 type = newType;
6120 }
6121 }
6122 } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
6123 final Locale locale = getCustomLocaleForKeyListenerOrNull();
6124 switch (type & EditorInfo.TYPE_MASK_VARIATION) {
6125 case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
6126 input = DateKeyListener.getInstance(locale);
6127 break;
6128 case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
6129 input = TimeKeyListener.getInstance(locale);
6130 break;
6131 default:
6132 input = DateTimeKeyListener.getInstance(locale);
6133 break;
6134 }
6135 if (mUseInternationalizedInput) {
6136 type = input.getInputType(); // Override type, if necessary for i18n.
6137 }
6138 } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
6139 input = DialerKeyListener.getInstance();
6140 } else {
6141 input = TextKeyListener.getInstance();
6142 }
6143 setRawInputType(type);
6144 mListenerChanged = false;
6145 if (direct) {
6146 createEditorIfNeeded();
6147 mEditor.mKeyListener = input;
6148 } else {
6149 setKeyListenerOnly(input);
6150 }
6151 }
6152
6153 /**
6154 * Get the type of the editable content.
6155 *
6156 * @see #setInputType(int)
6157 * @see android.text.InputType
6158 */
6159 public int getInputType() {
6160 return mEditor == null ? EditorInfo.TYPE_NULL : mEditor.mInputType;
6161 }
6162
6163 /**
6164 * Change the editor type integer associated with the text view, which
6165 * is reported to an Input Method Editor (IME) with {@link EditorInfo#imeOptions}
6166 * when it has focus.
6167 * @see #getImeOptions
6168 * @see android.view.inputmethod.EditorInfo
6169 * @attr ref android.R.styleable#TextView_imeOptions
6170 */
6171 public void setImeOptions(int imeOptions) {
6172 createEditorIfNeeded();
6173 mEditor.createInputContentTypeIfNeeded();
6174 mEditor.mInputContentType.imeOptions = imeOptions;
6175 }
6176
6177 /**
6178 * Get the type of the Input Method Editor (IME).
6179 * @return the type of the IME
6180 * @see #setImeOptions(int)
6181 * @see android.view.inputmethod.EditorInfo
6182 */
6183 public int getImeOptions() {
6184 return mEditor != null && mEditor.mInputContentType != null
6185 ? mEditor.mInputContentType.imeOptions : EditorInfo.IME_NULL;
6186 }
6187
6188 /**
6189 * Change the custom IME action associated with the text view, which
6190 * will be reported to an IME with {@link EditorInfo#actionLabel}
6191 * and {@link EditorInfo#actionId} when it has focus.
6192 * @see #getImeActionLabel
6193 * @see #getImeActionId
6194 * @see android.view.inputmethod.EditorInfo
6195 * @attr ref android.R.styleable#TextView_imeActionLabel
6196 * @attr ref android.R.styleable#TextView_imeActionId
6197 */
6198 public void setImeActionLabel(CharSequence label, int actionId) {
6199 createEditorIfNeeded();
6200 mEditor.createInputContentTypeIfNeeded();
6201 mEditor.mInputContentType.imeActionLabel = label;
6202 mEditor.mInputContentType.imeActionId = actionId;
6203 }
6204
6205 /**
6206 * Get the IME action label previous set with {@link #setImeActionLabel}.
6207 *
6208 * @see #setImeActionLabel
6209 * @see android.view.inputmethod.EditorInfo
6210 */
6211 public CharSequence getImeActionLabel() {
6212 return mEditor != null && mEditor.mInputContentType != null
6213 ? mEditor.mInputContentType.imeActionLabel : null;
6214 }
6215
6216 /**
6217 * Get the IME action ID previous set with {@link #setImeActionLabel}.
6218 *
6219 * @see #setImeActionLabel
6220 * @see android.view.inputmethod.EditorInfo
6221 */
6222 public int getImeActionId() {
6223 return mEditor != null && mEditor.mInputContentType != null
6224 ? mEditor.mInputContentType.imeActionId : 0;
6225 }
6226
6227 /**
6228 * Set a special listener to be called when an action is performed
6229 * on the text view. This will be called when the enter key is pressed,
6230 * or when an action supplied to the IME is selected by the user. Setting
6231 * this means that the normal hard key event will not insert a newline
6232 * into the text view, even if it is multi-line; holding down the ALT
6233 * modifier will, however, allow the user to insert a newline character.
6234 */
6235 public void setOnEditorActionListener(OnEditorActionListener l) {
6236 createEditorIfNeeded();
6237 mEditor.createInputContentTypeIfNeeded();
6238 mEditor.mInputContentType.onEditorActionListener = l;
6239 }
6240
6241 /**
6242 * Called when an attached input method calls
6243 * {@link InputConnection#performEditorAction(int)
6244 * InputConnection.performEditorAction()}
6245 * for this text view. The default implementation will call your action
6246 * listener supplied to {@link #setOnEditorActionListener}, or perform
6247 * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
6248 * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
6249 * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
6250 * EditorInfo.IME_ACTION_DONE}.
6251 *
6252 * <p>For backwards compatibility, if no IME options have been set and the
6253 * text view would not normally advance focus on enter, then
6254 * the NEXT and DONE actions received here will be turned into an enter
6255 * key down/up pair to go through the normal key handling.
6256 *
6257 * @param actionCode The code of the action being performed.
6258 *
6259 * @see #setOnEditorActionListener
6260 */
6261 public void onEditorAction(int actionCode) {
6262 final Editor.InputContentType ict = mEditor == null ? null : mEditor.mInputContentType;
6263 if (ict != null) {
6264 if (ict.onEditorActionListener != null) {
6265 if (ict.onEditorActionListener.onEditorAction(this,
6266 actionCode, null)) {
6267 return;
6268 }
6269 }
6270
6271 // This is the handling for some default action.
6272 // Note that for backwards compatibility we don't do this
6273 // default handling if explicit ime options have not been given,
6274 // instead turning this into the normal enter key codes that an
6275 // app may be expecting.
6276 if (actionCode == EditorInfo.IME_ACTION_NEXT) {
6277 View v = focusSearch(FOCUS_FORWARD);
6278 if (v != null) {
6279 if (!v.requestFocus(FOCUS_FORWARD)) {
6280 throw new IllegalStateException("focus search returned a view "
6281 + "that wasn't able to take focus!");
6282 }
6283 }
6284 return;
6285
6286 } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
6287 View v = focusSearch(FOCUS_BACKWARD);
6288 if (v != null) {
6289 if (!v.requestFocus(FOCUS_BACKWARD)) {
6290 throw new IllegalStateException("focus search returned a view "
6291 + "that wasn't able to take focus!");
6292 }
6293 }
6294 return;
6295
6296 } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
6297 InputMethodManager imm = InputMethodManager.peekInstance();
6298 if (imm != null && imm.isActive(this)) {
6299 imm.hideSoftInputFromWindow(getWindowToken(), 0);
6300 }
6301 return;
6302 }
6303 }
6304
6305 ViewRootImpl viewRootImpl = getViewRootImpl();
6306 if (viewRootImpl != null) {
6307 long eventTime = SystemClock.uptimeMillis();
6308 viewRootImpl.dispatchKeyFromIme(
6309 new KeyEvent(eventTime, eventTime,
6310 KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
6311 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
6312 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
6313 | KeyEvent.FLAG_EDITOR_ACTION));
6314 viewRootImpl.dispatchKeyFromIme(
6315 new KeyEvent(SystemClock.uptimeMillis(), eventTime,
6316 KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
6317 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
6318 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
6319 | KeyEvent.FLAG_EDITOR_ACTION));
6320 }
6321 }
6322
6323 /**
6324 * Set the private content type of the text, which is the
6325 * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
6326 * field that will be filled in when creating an input connection.
6327 *
6328 * @see #getPrivateImeOptions()
6329 * @see EditorInfo#privateImeOptions
6330 * @attr ref android.R.styleable#TextView_privateImeOptions
6331 */
6332 public void setPrivateImeOptions(String type) {
6333 createEditorIfNeeded();
6334 mEditor.createInputContentTypeIfNeeded();
6335 mEditor.mInputContentType.privateImeOptions = type;
6336 }
6337
6338 /**
6339 * Get the private type of the content.
6340 *
6341 * @see #setPrivateImeOptions(String)
6342 * @see EditorInfo#privateImeOptions
6343 */
6344 public String getPrivateImeOptions() {
6345 return mEditor != null && mEditor.mInputContentType != null
6346 ? mEditor.mInputContentType.privateImeOptions : null;
6347 }
6348
6349 /**
6350 * Set the extra input data of the text, which is the
6351 * {@link EditorInfo#extras TextBoxAttribute.extras}
6352 * Bundle that will be filled in when creating an input connection. The
6353 * given integer is the resource identifier of an XML resource holding an
6354 * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
6355 *
6356 * @see #getInputExtras(boolean)
6357 * @see EditorInfo#extras
6358 * @attr ref android.R.styleable#TextView_editorExtras
6359 */
6360 public void setInputExtras(@XmlRes int xmlResId) throws XmlPullParserException, IOException {
6361 createEditorIfNeeded();
6362 XmlResourceParser parser = getResources().getXml(xmlResId);
6363 mEditor.createInputContentTypeIfNeeded();
6364 mEditor.mInputContentType.extras = new Bundle();
6365 getResources().parseBundleExtras(parser, mEditor.mInputContentType.extras);
6366 }
6367
6368 /**
6369 * Retrieve the input extras currently associated with the text view, which
6370 * can be viewed as well as modified.
6371 *
6372 * @param create If true, the extras will be created if they don't already
6373 * exist. Otherwise, null will be returned if none have been created.
6374 * @see #setInputExtras(int)
6375 * @see EditorInfo#extras
6376 * @attr ref android.R.styleable#TextView_editorExtras
6377 */
6378 public Bundle getInputExtras(boolean create) {
6379 if (mEditor == null && !create) return null;
6380 createEditorIfNeeded();
6381 if (mEditor.mInputContentType == null) {
6382 if (!create) return null;
6383 mEditor.createInputContentTypeIfNeeded();
6384 }
6385 if (mEditor.mInputContentType.extras == null) {
6386 if (!create) return null;
6387 mEditor.mInputContentType.extras = new Bundle();
6388 }
6389 return mEditor.mInputContentType.extras;
6390 }
6391
6392 /**
6393 * Change "hint" locales associated with the text view, which will be reported to an IME with
6394 * {@link EditorInfo#hintLocales} when it has focus.
6395 *
6396 * Starting with Android O, this also causes internationalized listeners to be created (or
6397 * change locale) based on the first locale in the input locale list.
6398 *
6399 * <p><strong>Note:</strong> If you want new "hint" to take effect immediately you need to
6400 * call {@link InputMethodManager#restartInput(View)}.</p>
6401 * @param hintLocales List of the languages that the user is supposed to switch to no matter
6402 * what input method subtype is currently used. Set {@code null} to clear the current "hint".
6403 * @see #getImeHintLocales()
6404 * @see android.view.inputmethod.EditorInfo#hintLocales
6405 */
6406 public void setImeHintLocales(@Nullable LocaleList hintLocales) {
6407 createEditorIfNeeded();
6408 mEditor.createInputContentTypeIfNeeded();
6409 mEditor.mInputContentType.imeHintLocales = hintLocales;
6410 if (mUseInternationalizedInput) {
6411 changeListenerLocaleTo(hintLocales == null ? null : hintLocales.get(0));
6412 }
6413 }
6414
6415 /**
6416 * @return The current languages list "hint". {@code null} when no "hint" is available.
6417 * @see #setImeHintLocales(LocaleList)
6418 * @see android.view.inputmethod.EditorInfo#hintLocales
6419 */
6420 @Nullable
6421 public LocaleList getImeHintLocales() {
6422 if (mEditor == null) {
6423 return null;
6424 }
6425 if (mEditor.mInputContentType == null) {
6426 return null;
6427 }
6428 return mEditor.mInputContentType.imeHintLocales;
6429 }
6430
6431 /**
6432 * Returns the error message that was set to be displayed with
6433 * {@link #setError}, or <code>null</code> if no error was set
6434 * or if it the error was cleared by the widget after user input.
6435 */
6436 public CharSequence getError() {
6437 return mEditor == null ? null : mEditor.mError;
6438 }
6439
6440 /**
6441 * Sets the right-hand compound drawable of the TextView to the "error"
6442 * icon and sets an error message that will be displayed in a popup when
6443 * the TextView has focus. The icon and error message will be reset to
6444 * null when any key events cause changes to the TextView's text. If the
6445 * <code>error</code> is <code>null</code>, the error message and icon
6446 * will be cleared.
6447 */
6448 @android.view.RemotableViewMethod
6449 public void setError(CharSequence error) {
6450 if (error == null) {
6451 setError(null, null);
6452 } else {
6453 Drawable dr = getContext().getDrawable(
6454 com.android.internal.R.drawable.indicator_input_error);
6455
6456 dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
6457 setError(error, dr);
6458 }
6459 }
6460
6461 /**
6462 * Sets the right-hand compound drawable of the TextView to the specified
6463 * icon and sets an error message that will be displayed in a popup when
6464 * the TextView has focus. The icon and error message will be reset to
6465 * null when any key events cause changes to the TextView's text. The
6466 * drawable must already have had {@link Drawable#setBounds} set on it.
6467 * If the <code>error</code> is <code>null</code>, the error message will
6468 * be cleared (and you should provide a <code>null</code> icon as well).
6469 */
6470 public void setError(CharSequence error, Drawable icon) {
6471 createEditorIfNeeded();
6472 mEditor.setError(error, icon);
Justin Klaassen4d01eea2018-04-03 23:21:57 -04006473 notifyViewAccessibilityStateChangedIfNeeded(
Justin Klaassen10d07c82017-09-15 17:58:39 -04006474 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
6475 }
6476
6477 @Override
6478 protected boolean setFrame(int l, int t, int r, int b) {
6479 boolean result = super.setFrame(l, t, r, b);
6480
6481 if (mEditor != null) mEditor.setFrame();
6482
6483 restartMarqueeIfNeeded();
6484
6485 return result;
6486 }
6487
6488 private void restartMarqueeIfNeeded() {
6489 if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
6490 mRestartMarquee = false;
6491 startMarquee();
6492 }
6493 }
6494
6495 /**
6496 * Sets the list of input filters that will be used if the buffer is
6497 * Editable. Has no effect otherwise.
6498 *
6499 * @attr ref android.R.styleable#TextView_maxLength
6500 */
6501 public void setFilters(InputFilter[] filters) {
6502 if (filters == null) {
6503 throw new IllegalArgumentException();
6504 }
6505
6506 mFilters = filters;
6507
6508 if (mText instanceof Editable) {
6509 setFilters((Editable) mText, filters);
6510 }
6511 }
6512
6513 /**
6514 * Sets the list of input filters on the specified Editable,
6515 * and includes mInput in the list if it is an InputFilter.
6516 */
6517 private void setFilters(Editable e, InputFilter[] filters) {
6518 if (mEditor != null) {
6519 final boolean undoFilter = mEditor.mUndoInputFilter != null;
6520 final boolean keyFilter = mEditor.mKeyListener instanceof InputFilter;
6521 int num = 0;
6522 if (undoFilter) num++;
6523 if (keyFilter) num++;
6524 if (num > 0) {
6525 InputFilter[] nf = new InputFilter[filters.length + num];
6526
6527 System.arraycopy(filters, 0, nf, 0, filters.length);
6528 num = 0;
6529 if (undoFilter) {
6530 nf[filters.length] = mEditor.mUndoInputFilter;
6531 num++;
6532 }
6533 if (keyFilter) {
6534 nf[filters.length + num] = (InputFilter) mEditor.mKeyListener;
6535 }
6536
6537 e.setFilters(nf);
6538 return;
6539 }
6540 }
6541 e.setFilters(filters);
6542 }
6543
6544 /**
6545 * Returns the current list of input filters.
6546 *
6547 * @attr ref android.R.styleable#TextView_maxLength
6548 */
6549 public InputFilter[] getFilters() {
6550 return mFilters;
6551 }
6552
6553 /////////////////////////////////////////////////////////////////////////
6554
6555 private int getBoxHeight(Layout l) {
6556 Insets opticalInsets = isLayoutModeOptical(mParent) ? getOpticalInsets() : Insets.NONE;
6557 int padding = (l == mHintLayout)
6558 ? getCompoundPaddingTop() + getCompoundPaddingBottom()
6559 : getExtendedPaddingTop() + getExtendedPaddingBottom();
6560 return getMeasuredHeight() - padding + opticalInsets.top + opticalInsets.bottom;
6561 }
6562
6563 int getVerticalOffset(boolean forceNormal) {
6564 int voffset = 0;
6565 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
6566
6567 Layout l = mLayout;
6568 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
6569 l = mHintLayout;
6570 }
6571
6572 if (gravity != Gravity.TOP) {
6573 int boxht = getBoxHeight(l);
6574 int textht = l.getHeight();
6575
6576 if (textht < boxht) {
6577 if (gravity == Gravity.BOTTOM) {
6578 voffset = boxht - textht;
6579 } else { // (gravity == Gravity.CENTER_VERTICAL)
6580 voffset = (boxht - textht) >> 1;
6581 }
6582 }
6583 }
6584 return voffset;
6585 }
6586
6587 private int getBottomVerticalOffset(boolean forceNormal) {
6588 int voffset = 0;
6589 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
6590
6591 Layout l = mLayout;
6592 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
6593 l = mHintLayout;
6594 }
6595
6596 if (gravity != Gravity.BOTTOM) {
6597 int boxht = getBoxHeight(l);
6598 int textht = l.getHeight();
6599
6600 if (textht < boxht) {
6601 if (gravity == Gravity.TOP) {
6602 voffset = boxht - textht;
6603 } else { // (gravity == Gravity.CENTER_VERTICAL)
6604 voffset = (boxht - textht) >> 1;
6605 }
6606 }
6607 }
6608 return voffset;
6609 }
6610
6611 void invalidateCursorPath() {
6612 if (mHighlightPathBogus) {
6613 invalidateCursor();
6614 } else {
6615 final int horizontalPadding = getCompoundPaddingLeft();
6616 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
6617
Justin Klaassen93b7ee42017-10-10 15:20:13 -04006618 if (mEditor.mDrawableForCursor == null) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04006619 synchronized (TEMP_RECTF) {
6620 /*
6621 * The reason for this concern about the thickness of the
6622 * cursor and doing the floor/ceil on the coordinates is that
6623 * some EditTexts (notably textfields in the Browser) have
6624 * anti-aliased text where not all the characters are
6625 * necessarily at integer-multiple locations. This should
6626 * make sure the entire cursor gets invalidated instead of
6627 * sometimes missing half a pixel.
6628 */
6629 float thick = (float) Math.ceil(mTextPaint.getStrokeWidth());
6630 if (thick < 1.0f) {
6631 thick = 1.0f;
6632 }
6633
6634 thick /= 2.0f;
6635
6636 // mHighlightPath is guaranteed to be non null at that point.
6637 mHighlightPath.computeBounds(TEMP_RECTF, false);
6638
6639 invalidate((int) Math.floor(horizontalPadding + TEMP_RECTF.left - thick),
6640 (int) Math.floor(verticalPadding + TEMP_RECTF.top - thick),
6641 (int) Math.ceil(horizontalPadding + TEMP_RECTF.right + thick),
6642 (int) Math.ceil(verticalPadding + TEMP_RECTF.bottom + thick));
6643 }
6644 } else {
Justin Klaassen93b7ee42017-10-10 15:20:13 -04006645 final Rect bounds = mEditor.mDrawableForCursor.getBounds();
Justin Klaassen10d07c82017-09-15 17:58:39 -04006646 invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
6647 bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
6648 }
6649 }
6650 }
6651
6652 void invalidateCursor() {
6653 int where = getSelectionEnd();
6654
6655 invalidateCursor(where, where, where);
6656 }
6657
6658 private void invalidateCursor(int a, int b, int c) {
6659 if (a >= 0 || b >= 0 || c >= 0) {
6660 int start = Math.min(Math.min(a, b), c);
6661 int end = Math.max(Math.max(a, b), c);
6662 invalidateRegion(start, end, true /* Also invalidates blinking cursor */);
6663 }
6664 }
6665
6666 /**
6667 * Invalidates the region of text enclosed between the start and end text offsets.
6668 */
6669 void invalidateRegion(int start, int end, boolean invalidateCursor) {
6670 if (mLayout == null) {
6671 invalidate();
6672 } else {
6673 int lineStart = mLayout.getLineForOffset(start);
6674 int top = mLayout.getLineTop(lineStart);
6675
6676 // This is ridiculous, but the descent from the line above
6677 // can hang down into the line we really want to redraw,
6678 // so we have to invalidate part of the line above to make
6679 // sure everything that needs to be redrawn really is.
6680 // (But not the whole line above, because that would cause
6681 // the same problem with the descenders on the line above it!)
6682 if (lineStart > 0) {
6683 top -= mLayout.getLineDescent(lineStart - 1);
6684 }
6685
6686 int lineEnd;
6687
6688 if (start == end) {
6689 lineEnd = lineStart;
6690 } else {
6691 lineEnd = mLayout.getLineForOffset(end);
6692 }
6693
6694 int bottom = mLayout.getLineBottom(lineEnd);
6695
6696 // mEditor can be null in case selection is set programmatically.
Justin Klaassen93b7ee42017-10-10 15:20:13 -04006697 if (invalidateCursor && mEditor != null && mEditor.mDrawableForCursor != null) {
6698 final Rect bounds = mEditor.mDrawableForCursor.getBounds();
Justin Klaassen10d07c82017-09-15 17:58:39 -04006699 top = Math.min(top, bounds.top);
6700 bottom = Math.max(bottom, bounds.bottom);
6701 }
6702
6703 final int compoundPaddingLeft = getCompoundPaddingLeft();
6704 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
6705
6706 int left, right;
6707 if (lineStart == lineEnd && !invalidateCursor) {
6708 left = (int) mLayout.getPrimaryHorizontal(start);
6709 right = (int) (mLayout.getPrimaryHorizontal(end) + 1.0);
6710 left += compoundPaddingLeft;
6711 right += compoundPaddingLeft;
6712 } else {
6713 // Rectangle bounding box when the region spans several lines
6714 left = compoundPaddingLeft;
6715 right = getWidth() - getCompoundPaddingRight();
6716 }
6717
6718 invalidate(mScrollX + left, verticalPadding + top,
6719 mScrollX + right, verticalPadding + bottom);
6720 }
6721 }
6722
6723 private void registerForPreDraw() {
6724 if (!mPreDrawRegistered) {
6725 getViewTreeObserver().addOnPreDrawListener(this);
6726 mPreDrawRegistered = true;
6727 }
6728 }
6729
6730 private void unregisterForPreDraw() {
6731 getViewTreeObserver().removeOnPreDrawListener(this);
6732 mPreDrawRegistered = false;
6733 mPreDrawListenerDetached = false;
6734 }
6735
6736 /**
6737 * {@inheritDoc}
6738 */
6739 @Override
6740 public boolean onPreDraw() {
6741 if (mLayout == null) {
6742 assumeLayout();
6743 }
6744
6745 if (mMovement != null) {
6746 /* This code also provides auto-scrolling when a cursor is moved using a
6747 * CursorController (insertion point or selection limits).
6748 * For selection, ensure start or end is visible depending on controller's state.
6749 */
6750 int curs = getSelectionEnd();
6751 // Do not create the controller if it is not already created.
6752 if (mEditor != null && mEditor.mSelectionModifierCursorController != null
6753 && mEditor.mSelectionModifierCursorController.isSelectionStartDragged()) {
6754 curs = getSelectionStart();
6755 }
6756
6757 /*
6758 * TODO: This should really only keep the end in view if
6759 * it already was before the text changed. I'm not sure
6760 * of a good way to tell from here if it was.
6761 */
6762 if (curs < 0 && (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6763 curs = mText.length();
6764 }
6765
6766 if (curs >= 0) {
6767 bringPointIntoView(curs);
6768 }
6769 } else {
6770 bringTextIntoView();
6771 }
6772
6773 // This has to be checked here since:
6774 // - onFocusChanged cannot start it when focus is given to a view with selected text (after
6775 // a screen rotation) since layout is not yet initialized at that point.
6776 if (mEditor != null && mEditor.mCreatedWithASelection) {
6777 mEditor.refreshTextActionMode();
6778 mEditor.mCreatedWithASelection = false;
6779 }
6780
6781 unregisterForPreDraw();
6782
6783 return true;
6784 }
6785
6786 @Override
6787 protected void onAttachedToWindow() {
6788 super.onAttachedToWindow();
6789
6790 if (mEditor != null) mEditor.onAttachedToWindow();
6791
6792 if (mPreDrawListenerDetached) {
6793 getViewTreeObserver().addOnPreDrawListener(this);
6794 mPreDrawListenerDetached = false;
6795 }
6796 }
6797
6798 /** @hide */
6799 @Override
6800 protected void onDetachedFromWindowInternal() {
6801 if (mPreDrawRegistered) {
6802 getViewTreeObserver().removeOnPreDrawListener(this);
6803 mPreDrawListenerDetached = true;
6804 }
6805
6806 resetResolvedDrawables();
6807
6808 if (mEditor != null) mEditor.onDetachedFromWindow();
6809
6810 super.onDetachedFromWindowInternal();
6811 }
6812
6813 @Override
6814 public void onScreenStateChanged(int screenState) {
6815 super.onScreenStateChanged(screenState);
6816 if (mEditor != null) mEditor.onScreenStateChanged(screenState);
6817 }
6818
6819 @Override
6820 protected boolean isPaddingOffsetRequired() {
6821 return mShadowRadius != 0 || mDrawables != null;
6822 }
6823
6824 @Override
6825 protected int getLeftPaddingOffset() {
6826 return getCompoundPaddingLeft() - mPaddingLeft
6827 + (int) Math.min(0, mShadowDx - mShadowRadius);
6828 }
6829
6830 @Override
6831 protected int getTopPaddingOffset() {
6832 return (int) Math.min(0, mShadowDy - mShadowRadius);
6833 }
6834
6835 @Override
6836 protected int getBottomPaddingOffset() {
6837 return (int) Math.max(0, mShadowDy + mShadowRadius);
6838 }
6839
6840 @Override
6841 protected int getRightPaddingOffset() {
6842 return -(getCompoundPaddingRight() - mPaddingRight)
6843 + (int) Math.max(0, mShadowDx + mShadowRadius);
6844 }
6845
6846 @Override
6847 protected boolean verifyDrawable(@NonNull Drawable who) {
6848 final boolean verified = super.verifyDrawable(who);
6849 if (!verified && mDrawables != null) {
6850 for (Drawable dr : mDrawables.mShowing) {
6851 if (who == dr) {
6852 return true;
6853 }
6854 }
6855 }
6856 return verified;
6857 }
6858
6859 @Override
6860 public void jumpDrawablesToCurrentState() {
6861 super.jumpDrawablesToCurrentState();
6862 if (mDrawables != null) {
6863 for (Drawable dr : mDrawables.mShowing) {
6864 if (dr != null) {
6865 dr.jumpToCurrentState();
6866 }
6867 }
6868 }
6869 }
6870
6871 @Override
6872 public void invalidateDrawable(@NonNull Drawable drawable) {
6873 boolean handled = false;
6874
6875 if (verifyDrawable(drawable)) {
6876 final Rect dirty = drawable.getBounds();
6877 int scrollX = mScrollX;
6878 int scrollY = mScrollY;
6879
6880 // IMPORTANT: The coordinates below are based on the coordinates computed
6881 // for each compound drawable in onDraw(). Make sure to update each section
6882 // accordingly.
6883 final TextView.Drawables drawables = mDrawables;
6884 if (drawables != null) {
6885 if (drawable == drawables.mShowing[Drawables.LEFT]) {
6886 final int compoundPaddingTop = getCompoundPaddingTop();
6887 final int compoundPaddingBottom = getCompoundPaddingBottom();
6888 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
6889
6890 scrollX += mPaddingLeft;
6891 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
6892 handled = true;
6893 } else if (drawable == drawables.mShowing[Drawables.RIGHT]) {
6894 final int compoundPaddingTop = getCompoundPaddingTop();
6895 final int compoundPaddingBottom = getCompoundPaddingBottom();
6896 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
6897
6898 scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
6899 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
6900 handled = true;
6901 } else if (drawable == drawables.mShowing[Drawables.TOP]) {
6902 final int compoundPaddingLeft = getCompoundPaddingLeft();
6903 final int compoundPaddingRight = getCompoundPaddingRight();
6904 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
6905
6906 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
6907 scrollY += mPaddingTop;
6908 handled = true;
6909 } else if (drawable == drawables.mShowing[Drawables.BOTTOM]) {
6910 final int compoundPaddingLeft = getCompoundPaddingLeft();
6911 final int compoundPaddingRight = getCompoundPaddingRight();
6912 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
6913
6914 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
6915 scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
6916 handled = true;
6917 }
6918 }
6919
6920 if (handled) {
6921 invalidate(dirty.left + scrollX, dirty.top + scrollY,
6922 dirty.right + scrollX, dirty.bottom + scrollY);
6923 }
6924 }
6925
6926 if (!handled) {
6927 super.invalidateDrawable(drawable);
6928 }
6929 }
6930
6931 @Override
6932 public boolean hasOverlappingRendering() {
6933 // horizontal fading edge causes SaveLayerAlpha, which doesn't support alpha modulation
6934 return ((getBackground() != null && getBackground().getCurrent() != null)
Justin Klaassen4d01eea2018-04-03 23:21:57 -04006935 || mSpannable != null || hasSelection() || isHorizontalFadingEdgeEnabled());
Justin Klaassen10d07c82017-09-15 17:58:39 -04006936 }
6937
6938 /**
6939 *
6940 * Returns the state of the {@code textIsSelectable} flag (See
6941 * {@link #setTextIsSelectable setTextIsSelectable()}). Although you have to set this flag
6942 * to allow users to select and copy text in a non-editable TextView, the content of an
6943 * {@link EditText} can always be selected, independently of the value of this flag.
6944 * <p>
6945 *
6946 * @return True if the text displayed in this TextView can be selected by the user.
6947 *
6948 * @attr ref android.R.styleable#TextView_textIsSelectable
6949 */
6950 public boolean isTextSelectable() {
6951 return mEditor == null ? false : mEditor.mTextIsSelectable;
6952 }
6953
6954 /**
6955 * Sets whether the content of this view is selectable by the user. The default is
6956 * {@code false}, meaning that the content is not selectable.
6957 * <p>
6958 * When you use a TextView to display a useful piece of information to the user (such as a
6959 * contact's address), make it selectable, so that the user can select and copy its
6960 * content. You can also use set the XML attribute
6961 * {@link android.R.styleable#TextView_textIsSelectable} to "true".
6962 * <p>
6963 * When you call this method to set the value of {@code textIsSelectable}, it sets
6964 * the flags {@code focusable}, {@code focusableInTouchMode}, {@code clickable},
6965 * and {@code longClickable} to the same value. These flags correspond to the attributes
6966 * {@link android.R.styleable#View_focusable android:focusable},
6967 * {@link android.R.styleable#View_focusableInTouchMode android:focusableInTouchMode},
6968 * {@link android.R.styleable#View_clickable android:clickable}, and
6969 * {@link android.R.styleable#View_longClickable android:longClickable}. To restore any of these
6970 * flags to a state you had set previously, call one or more of the following methods:
6971 * {@link #setFocusable(boolean) setFocusable()},
6972 * {@link #setFocusableInTouchMode(boolean) setFocusableInTouchMode()},
6973 * {@link #setClickable(boolean) setClickable()} or
6974 * {@link #setLongClickable(boolean) setLongClickable()}.
6975 *
6976 * @param selectable Whether the content of this TextView should be selectable.
6977 */
6978 public void setTextIsSelectable(boolean selectable) {
6979 if (!selectable && mEditor == null) return; // false is default value with no edit data
6980
6981 createEditorIfNeeded();
6982 if (mEditor.mTextIsSelectable == selectable) return;
6983
6984 mEditor.mTextIsSelectable = selectable;
6985 setFocusableInTouchMode(selectable);
6986 setFocusable(FOCUSABLE_AUTO);
6987 setClickable(selectable);
6988 setLongClickable(selectable);
6989
6990 // mInputType should already be EditorInfo.TYPE_NULL and mInput should be null
6991
6992 setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
6993 setText(mText, selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
6994
6995 // Called by setText above, but safer in case of future code changes
6996 mEditor.prepareCursorControllers();
6997 }
6998
6999 @Override
7000 protected int[] onCreateDrawableState(int extraSpace) {
7001 final int[] drawableState;
7002
7003 if (mSingleLine) {
7004 drawableState = super.onCreateDrawableState(extraSpace);
7005 } else {
7006 drawableState = super.onCreateDrawableState(extraSpace + 1);
7007 mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
7008 }
7009
7010 if (isTextSelectable()) {
7011 // Disable pressed state, which was introduced when TextView was made clickable.
7012 // Prevents text color change.
7013 // setClickable(false) would have a similar effect, but it also disables focus changes
7014 // and long press actions, which are both needed by text selection.
7015 final int length = drawableState.length;
7016 for (int i = 0; i < length; i++) {
7017 if (drawableState[i] == R.attr.state_pressed) {
7018 final int[] nonPressedState = new int[length - 1];
7019 System.arraycopy(drawableState, 0, nonPressedState, 0, i);
7020 System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
7021 return nonPressedState;
7022 }
7023 }
7024 }
7025
7026 return drawableState;
7027 }
7028
7029 private Path getUpdatedHighlightPath() {
7030 Path highlight = null;
7031 Paint highlightPaint = mHighlightPaint;
7032
7033 final int selStart = getSelectionStart();
7034 final int selEnd = getSelectionEnd();
7035 if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) {
7036 if (selStart == selEnd) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04007037 if (mEditor != null && mEditor.shouldRenderCursor()) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04007038 if (mHighlightPathBogus) {
7039 if (mHighlightPath == null) mHighlightPath = new Path();
7040 mHighlightPath.reset();
7041 mLayout.getCursorPath(selStart, mHighlightPath, mText);
7042 mEditor.updateCursorPosition();
7043 mHighlightPathBogus = false;
7044 }
7045
7046 // XXX should pass to skin instead of drawing directly
7047 highlightPaint.setColor(mCurTextColor);
7048 highlightPaint.setStyle(Paint.Style.STROKE);
7049 highlight = mHighlightPath;
7050 }
7051 } else {
7052 if (mHighlightPathBogus) {
7053 if (mHighlightPath == null) mHighlightPath = new Path();
7054 mHighlightPath.reset();
7055 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
7056 mHighlightPathBogus = false;
7057 }
7058
7059 // XXX should pass to skin instead of drawing directly
7060 highlightPaint.setColor(mHighlightColor);
7061 highlightPaint.setStyle(Paint.Style.FILL);
7062
7063 highlight = mHighlightPath;
7064 }
7065 }
7066 return highlight;
7067 }
7068
7069 /**
7070 * @hide
7071 */
7072 public int getHorizontalOffsetForDrawables() {
7073 return 0;
7074 }
7075
7076 @Override
7077 protected void onDraw(Canvas canvas) {
7078 restartMarqueeIfNeeded();
7079
7080 // Draw the background for this view
7081 super.onDraw(canvas);
7082
7083 final int compoundPaddingLeft = getCompoundPaddingLeft();
7084 final int compoundPaddingTop = getCompoundPaddingTop();
7085 final int compoundPaddingRight = getCompoundPaddingRight();
7086 final int compoundPaddingBottom = getCompoundPaddingBottom();
7087 final int scrollX = mScrollX;
7088 final int scrollY = mScrollY;
7089 final int right = mRight;
7090 final int left = mLeft;
7091 final int bottom = mBottom;
7092 final int top = mTop;
7093 final boolean isLayoutRtl = isLayoutRtl();
7094 final int offset = getHorizontalOffsetForDrawables();
7095 final int leftOffset = isLayoutRtl ? 0 : offset;
7096 final int rightOffset = isLayoutRtl ? offset : 0;
7097
7098 final Drawables dr = mDrawables;
7099 if (dr != null) {
7100 /*
7101 * Compound, not extended, because the icon is not clipped
7102 * if the text height is smaller.
7103 */
7104
7105 int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
7106 int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
7107
7108 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
7109 // Make sure to update invalidateDrawable() when changing this code.
7110 if (dr.mShowing[Drawables.LEFT] != null) {
7111 canvas.save();
7112 canvas.translate(scrollX + mPaddingLeft + leftOffset,
7113 scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
7114 dr.mShowing[Drawables.LEFT].draw(canvas);
7115 canvas.restore();
7116 }
7117
7118 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
7119 // Make sure to update invalidateDrawable() when changing this code.
7120 if (dr.mShowing[Drawables.RIGHT] != null) {
7121 canvas.save();
7122 canvas.translate(scrollX + right - left - mPaddingRight
7123 - dr.mDrawableSizeRight - rightOffset,
7124 scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
7125 dr.mShowing[Drawables.RIGHT].draw(canvas);
7126 canvas.restore();
7127 }
7128
7129 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
7130 // Make sure to update invalidateDrawable() when changing this code.
7131 if (dr.mShowing[Drawables.TOP] != null) {
7132 canvas.save();
7133 canvas.translate(scrollX + compoundPaddingLeft
7134 + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);
7135 dr.mShowing[Drawables.TOP].draw(canvas);
7136 canvas.restore();
7137 }
7138
7139 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
7140 // Make sure to update invalidateDrawable() when changing this code.
7141 if (dr.mShowing[Drawables.BOTTOM] != null) {
7142 canvas.save();
7143 canvas.translate(scrollX + compoundPaddingLeft
7144 + (hspace - dr.mDrawableWidthBottom) / 2,
7145 scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
7146 dr.mShowing[Drawables.BOTTOM].draw(canvas);
7147 canvas.restore();
7148 }
7149 }
7150
7151 int color = mCurTextColor;
7152
7153 if (mLayout == null) {
7154 assumeLayout();
7155 }
7156
7157 Layout layout = mLayout;
7158
7159 if (mHint != null && mText.length() == 0) {
7160 if (mHintTextColor != null) {
7161 color = mCurHintTextColor;
7162 }
7163
7164 layout = mHintLayout;
7165 }
7166
7167 mTextPaint.setColor(color);
7168 mTextPaint.drawableState = getDrawableState();
7169
7170 canvas.save();
7171 /* Would be faster if we didn't have to do this. Can we chop the
7172 (displayable) text so that we don't need to do this ever?
7173 */
7174
7175 int extendedPaddingTop = getExtendedPaddingTop();
7176 int extendedPaddingBottom = getExtendedPaddingBottom();
7177
7178 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
7179 final int maxScrollY = mLayout.getHeight() - vspace;
7180
7181 float clipLeft = compoundPaddingLeft + scrollX;
7182 float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
7183 float clipRight = right - left - getCompoundPaddingRight() + scrollX;
7184 float clipBottom = bottom - top + scrollY
7185 - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
7186
7187 if (mShadowRadius != 0) {
7188 clipLeft += Math.min(0, mShadowDx - mShadowRadius);
7189 clipRight += Math.max(0, mShadowDx + mShadowRadius);
7190
7191 clipTop += Math.min(0, mShadowDy - mShadowRadius);
7192 clipBottom += Math.max(0, mShadowDy + mShadowRadius);
7193 }
7194
7195 canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
7196
7197 int voffsetText = 0;
7198 int voffsetCursor = 0;
7199
7200 // translate in by our padding
7201 /* shortcircuit calling getVerticaOffset() */
7202 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
7203 voffsetText = getVerticalOffset(false);
7204 voffsetCursor = getVerticalOffset(true);
7205 }
7206 canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
7207
7208 final int layoutDirection = getLayoutDirection();
7209 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
7210 if (isMarqueeFadeEnabled()) {
7211 if (!mSingleLine && getLineCount() == 1 && canMarquee()
7212 && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
7213 final int width = mRight - mLeft;
7214 final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
7215 final float dx = mLayout.getLineRight(0) - (width - padding);
7216 canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
7217 }
7218
7219 if (mMarquee != null && mMarquee.isRunning()) {
7220 final float dx = -mMarquee.getScroll();
7221 canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
7222 }
7223 }
7224
7225 final int cursorOffsetVertical = voffsetCursor - voffsetText;
7226
7227 Path highlight = getUpdatedHighlightPath();
7228 if (mEditor != null) {
7229 mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
7230 } else {
7231 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
7232 }
7233
7234 if (mMarquee != null && mMarquee.shouldDrawGhost()) {
7235 final float dx = mMarquee.getGhostOffset();
7236 canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
7237 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
7238 }
7239
7240 canvas.restore();
7241 }
7242
7243 @Override
7244 public void getFocusedRect(Rect r) {
7245 if (mLayout == null) {
7246 super.getFocusedRect(r);
7247 return;
7248 }
7249
7250 int selEnd = getSelectionEnd();
7251 if (selEnd < 0) {
7252 super.getFocusedRect(r);
7253 return;
7254 }
7255
7256 int selStart = getSelectionStart();
7257 if (selStart < 0 || selStart >= selEnd) {
7258 int line = mLayout.getLineForOffset(selEnd);
7259 r.top = mLayout.getLineTop(line);
7260 r.bottom = mLayout.getLineBottom(line);
7261 r.left = (int) mLayout.getPrimaryHorizontal(selEnd) - 2;
7262 r.right = r.left + 4;
7263 } else {
7264 int lineStart = mLayout.getLineForOffset(selStart);
7265 int lineEnd = mLayout.getLineForOffset(selEnd);
7266 r.top = mLayout.getLineTop(lineStart);
7267 r.bottom = mLayout.getLineBottom(lineEnd);
7268 if (lineStart == lineEnd) {
7269 r.left = (int) mLayout.getPrimaryHorizontal(selStart);
7270 r.right = (int) mLayout.getPrimaryHorizontal(selEnd);
7271 } else {
7272 // Selection extends across multiple lines -- make the focused
7273 // rect cover the entire width.
7274 if (mHighlightPathBogus) {
7275 if (mHighlightPath == null) mHighlightPath = new Path();
7276 mHighlightPath.reset();
7277 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
7278 mHighlightPathBogus = false;
7279 }
7280 synchronized (TEMP_RECTF) {
7281 mHighlightPath.computeBounds(TEMP_RECTF, true);
7282 r.left = (int) TEMP_RECTF.left - 1;
7283 r.right = (int) TEMP_RECTF.right + 1;
7284 }
7285 }
7286 }
7287
7288 // Adjust for padding and gravity.
7289 int paddingLeft = getCompoundPaddingLeft();
7290 int paddingTop = getExtendedPaddingTop();
7291 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
7292 paddingTop += getVerticalOffset(false);
7293 }
7294 r.offset(paddingLeft, paddingTop);
7295 int paddingBottom = getExtendedPaddingBottom();
7296 r.bottom += paddingBottom;
7297 }
7298
7299 /**
7300 * Return the number of lines of text, or 0 if the internal Layout has not
7301 * been built.
7302 */
7303 public int getLineCount() {
7304 return mLayout != null ? mLayout.getLineCount() : 0;
7305 }
7306
7307 /**
7308 * Return the baseline for the specified line (0...getLineCount() - 1)
7309 * If bounds is not null, return the top, left, right, bottom extents
7310 * of the specified line in it. If the internal Layout has not been built,
7311 * return 0 and set bounds to (0, 0, 0, 0)
7312 * @param line which line to examine (0..getLineCount() - 1)
7313 * @param bounds Optional. If not null, it returns the extent of the line
7314 * @return the Y-coordinate of the baseline
7315 */
7316 public int getLineBounds(int line, Rect bounds) {
7317 if (mLayout == null) {
7318 if (bounds != null) {
7319 bounds.set(0, 0, 0, 0);
7320 }
7321 return 0;
7322 } else {
7323 int baseline = mLayout.getLineBounds(line, bounds);
7324
7325 int voffset = getExtendedPaddingTop();
7326 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
7327 voffset += getVerticalOffset(true);
7328 }
7329 if (bounds != null) {
7330 bounds.offset(getCompoundPaddingLeft(), voffset);
7331 }
7332 return baseline + voffset;
7333 }
7334 }
7335
7336 @Override
7337 public int getBaseline() {
7338 if (mLayout == null) {
7339 return super.getBaseline();
7340 }
7341
7342 return getBaselineOffset() + mLayout.getLineBaseline(0);
7343 }
7344
7345 int getBaselineOffset() {
7346 int voffset = 0;
7347 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
7348 voffset = getVerticalOffset(true);
7349 }
7350
7351 if (isLayoutModeOptical(mParent)) {
7352 voffset -= getOpticalInsets().top;
7353 }
7354
7355 return getExtendedPaddingTop() + voffset;
7356 }
7357
7358 /**
7359 * @hide
7360 */
7361 @Override
7362 protected int getFadeTop(boolean offsetRequired) {
7363 if (mLayout == null) return 0;
7364
7365 int voffset = 0;
7366 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
7367 voffset = getVerticalOffset(true);
7368 }
7369
7370 if (offsetRequired) voffset += getTopPaddingOffset();
7371
7372 return getExtendedPaddingTop() + voffset;
7373 }
7374
7375 /**
7376 * @hide
7377 */
7378 @Override
7379 protected int getFadeHeight(boolean offsetRequired) {
7380 return mLayout != null ? mLayout.getHeight() : 0;
7381 }
7382
7383 @Override
7384 public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04007385 if (mSpannable != null && mLinksClickable) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04007386 final float x = event.getX(pointerIndex);
7387 final float y = event.getY(pointerIndex);
7388 final int offset = getOffsetForPosition(x, y);
Justin Klaassen4d01eea2018-04-03 23:21:57 -04007389 final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset,
Justin Klaassen10d07c82017-09-15 17:58:39 -04007390 ClickableSpan.class);
7391 if (clickables.length > 0) {
7392 return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND);
7393 }
7394 }
7395 if (isTextSelectable() || isTextEditable()) {
7396 return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT);
7397 }
7398 return super.onResolvePointerIcon(event, pointerIndex);
7399 }
7400
7401 @Override
7402 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
7403 // Note: If the IME is in fullscreen mode and IMS#mExtractEditText is in text action mode,
7404 // InputMethodService#onKeyDown and InputMethodService#onKeyUp are responsible to call
7405 // InputMethodService#mExtractEditText.maybeHandleBackInTextActionMode(event).
7406 if (keyCode == KeyEvent.KEYCODE_BACK && handleBackInTextActionModeIfNeeded(event)) {
7407 return true;
7408 }
7409 return super.onKeyPreIme(keyCode, event);
7410 }
7411
7412 /**
7413 * @hide
7414 */
7415 public boolean handleBackInTextActionModeIfNeeded(KeyEvent event) {
7416 // Do nothing unless mEditor is in text action mode.
7417 if (mEditor == null || mEditor.getTextActionMode() == null) {
7418 return false;
7419 }
7420
7421 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
7422 KeyEvent.DispatcherState state = getKeyDispatcherState();
7423 if (state != null) {
7424 state.startTracking(event, this);
7425 }
7426 return true;
7427 } else if (event.getAction() == KeyEvent.ACTION_UP) {
7428 KeyEvent.DispatcherState state = getKeyDispatcherState();
7429 if (state != null) {
7430 state.handleUpEvent(event);
7431 }
7432 if (event.isTracking() && !event.isCanceled()) {
7433 stopTextActionMode();
7434 return true;
7435 }
7436 }
7437 return false;
7438 }
7439
7440 @Override
7441 public boolean onKeyDown(int keyCode, KeyEvent event) {
7442 final int which = doKeyDown(keyCode, event, null);
7443 if (which == KEY_EVENT_NOT_HANDLED) {
7444 return super.onKeyDown(keyCode, event);
7445 }
7446
7447 return true;
7448 }
7449
7450 @Override
7451 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
7452 KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
7453 final int which = doKeyDown(keyCode, down, event);
7454 if (which == KEY_EVENT_NOT_HANDLED) {
7455 // Go through default dispatching.
7456 return super.onKeyMultiple(keyCode, repeatCount, event);
7457 }
7458 if (which == KEY_EVENT_HANDLED) {
7459 // Consumed the whole thing.
7460 return true;
7461 }
7462
7463 repeatCount--;
7464
7465 // We are going to dispatch the remaining events to either the input
7466 // or movement method. To do this, we will just send a repeated stream
7467 // of down and up events until we have done the complete repeatCount.
7468 // It would be nice if those interfaces had an onKeyMultiple() method,
7469 // but adding that is a more complicated change.
7470 KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
7471 if (which == KEY_DOWN_HANDLED_BY_KEY_LISTENER) {
7472 // mEditor and mEditor.mInput are not null from doKeyDown
7473 mEditor.mKeyListener.onKeyUp(this, (Editable) mText, keyCode, up);
7474 while (--repeatCount > 0) {
7475 mEditor.mKeyListener.onKeyDown(this, (Editable) mText, keyCode, down);
7476 mEditor.mKeyListener.onKeyUp(this, (Editable) mText, keyCode, up);
7477 }
7478 hideErrorIfUnchanged();
7479
7480 } else if (which == KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD) {
7481 // mMovement is not null from doKeyDown
Justin Klaassen4d01eea2018-04-03 23:21:57 -04007482 mMovement.onKeyUp(this, mSpannable, keyCode, up);
Justin Klaassen10d07c82017-09-15 17:58:39 -04007483 while (--repeatCount > 0) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04007484 mMovement.onKeyDown(this, mSpannable, keyCode, down);
7485 mMovement.onKeyUp(this, mSpannable, keyCode, up);
Justin Klaassen10d07c82017-09-15 17:58:39 -04007486 }
7487 }
7488
7489 return true;
7490 }
7491
7492 /**
7493 * Returns true if pressing ENTER in this field advances focus instead
7494 * of inserting the character. This is true mostly in single-line fields,
7495 * but also in mail addresses and subjects which will display on multiple
7496 * lines but where it doesn't make sense to insert newlines.
7497 */
7498 private boolean shouldAdvanceFocusOnEnter() {
7499 if (getKeyListener() == null) {
7500 return false;
7501 }
7502
7503 if (mSingleLine) {
7504 return true;
7505 }
7506
7507 if (mEditor != null
7508 && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
7509 == EditorInfo.TYPE_CLASS_TEXT) {
7510 int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
7511 if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
7512 || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
7513 return true;
7514 }
7515 }
7516
7517 return false;
7518 }
7519
7520 /**
7521 * Returns true if pressing TAB in this field advances focus instead
7522 * of inserting the character. Insert tabs only in multi-line editors.
7523 */
7524 private boolean shouldAdvanceFocusOnTab() {
7525 if (getKeyListener() != null && !mSingleLine && mEditor != null
7526 && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
7527 == EditorInfo.TYPE_CLASS_TEXT) {
7528 int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
7529 if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
7530 || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
7531 return false;
7532 }
7533 }
7534 return true;
7535 }
7536
7537 private boolean isDirectionalNavigationKey(int keyCode) {
7538 switch(keyCode) {
7539 case KeyEvent.KEYCODE_DPAD_UP:
7540 case KeyEvent.KEYCODE_DPAD_DOWN:
7541 case KeyEvent.KEYCODE_DPAD_LEFT:
7542 case KeyEvent.KEYCODE_DPAD_RIGHT:
7543 return true;
7544 }
7545 return false;
7546 }
7547
7548 private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
7549 if (!isEnabled()) {
7550 return KEY_EVENT_NOT_HANDLED;
7551 }
7552
7553 // If this is the initial keydown, we don't want to prevent a movement away from this view.
7554 // While this shouldn't be necessary because any time we're preventing default movement we
7555 // should be restricting the focus to remain within this view, thus we'll also receive
7556 // the key up event, occasionally key up events will get dropped and we don't want to
7557 // prevent the user from traversing out of this on the next key down.
7558 if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) {
7559 mPreventDefaultMovement = false;
7560 }
7561
7562 switch (keyCode) {
7563 case KeyEvent.KEYCODE_ENTER:
7564 if (event.hasNoModifiers()) {
7565 // When mInputContentType is set, we know that we are
7566 // running in a "modern" cupcake environment, so don't need
7567 // to worry about the application trying to capture
7568 // enter key events.
7569 if (mEditor != null && mEditor.mInputContentType != null) {
7570 // If there is an action listener, given them a
7571 // chance to consume the event.
7572 if (mEditor.mInputContentType.onEditorActionListener != null
7573 && mEditor.mInputContentType.onEditorActionListener.onEditorAction(
7574 this, EditorInfo.IME_NULL, event)) {
7575 mEditor.mInputContentType.enterDown = true;
7576 // We are consuming the enter key for them.
7577 return KEY_EVENT_HANDLED;
7578 }
7579 }
7580
7581 // If our editor should move focus when enter is pressed, or
7582 // this is a generated event from an IME action button, then
7583 // don't let it be inserted into the text.
7584 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
7585 || shouldAdvanceFocusOnEnter()) {
7586 if (hasOnClickListeners()) {
7587 return KEY_EVENT_NOT_HANDLED;
7588 }
7589 return KEY_EVENT_HANDLED;
7590 }
7591 }
7592 break;
7593
7594 case KeyEvent.KEYCODE_DPAD_CENTER:
7595 if (event.hasNoModifiers()) {
7596 if (shouldAdvanceFocusOnEnter()) {
7597 return KEY_EVENT_NOT_HANDLED;
7598 }
7599 }
7600 break;
7601
7602 case KeyEvent.KEYCODE_TAB:
7603 if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
7604 if (shouldAdvanceFocusOnTab()) {
7605 return KEY_EVENT_NOT_HANDLED;
7606 }
7607 }
7608 break;
7609
7610 // Has to be done on key down (and not on key up) to correctly be intercepted.
7611 case KeyEvent.KEYCODE_BACK:
7612 if (mEditor != null && mEditor.getTextActionMode() != null) {
7613 stopTextActionMode();
7614 return KEY_EVENT_HANDLED;
7615 }
7616 break;
7617
7618 case KeyEvent.KEYCODE_CUT:
7619 if (event.hasNoModifiers() && canCut()) {
7620 if (onTextContextMenuItem(ID_CUT)) {
7621 return KEY_EVENT_HANDLED;
7622 }
7623 }
7624 break;
7625
7626 case KeyEvent.KEYCODE_COPY:
7627 if (event.hasNoModifiers() && canCopy()) {
7628 if (onTextContextMenuItem(ID_COPY)) {
7629 return KEY_EVENT_HANDLED;
7630 }
7631 }
7632 break;
7633
7634 case KeyEvent.KEYCODE_PASTE:
7635 if (event.hasNoModifiers() && canPaste()) {
7636 if (onTextContextMenuItem(ID_PASTE)) {
7637 return KEY_EVENT_HANDLED;
7638 }
7639 }
7640 break;
7641 }
7642
7643 if (mEditor != null && mEditor.mKeyListener != null) {
7644 boolean doDown = true;
7645 if (otherEvent != null) {
7646 try {
7647 beginBatchEdit();
7648 final boolean handled = mEditor.mKeyListener.onKeyOther(this, (Editable) mText,
7649 otherEvent);
7650 hideErrorIfUnchanged();
7651 doDown = false;
7652 if (handled) {
7653 return KEY_EVENT_HANDLED;
7654 }
7655 } catch (AbstractMethodError e) {
7656 // onKeyOther was added after 1.0, so if it isn't
7657 // implemented we need to try to dispatch as a regular down.
7658 } finally {
7659 endBatchEdit();
7660 }
7661 }
7662
7663 if (doDown) {
7664 beginBatchEdit();
7665 final boolean handled = mEditor.mKeyListener.onKeyDown(this, (Editable) mText,
7666 keyCode, event);
7667 endBatchEdit();
7668 hideErrorIfUnchanged();
7669 if (handled) return KEY_DOWN_HANDLED_BY_KEY_LISTENER;
7670 }
7671 }
7672
7673 // bug 650865: sometimes we get a key event before a layout.
7674 // don't try to move around if we don't know the layout.
7675
7676 if (mMovement != null && mLayout != null) {
7677 boolean doDown = true;
7678 if (otherEvent != null) {
7679 try {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04007680 boolean handled = mMovement.onKeyOther(this, mSpannable, otherEvent);
Justin Klaassen10d07c82017-09-15 17:58:39 -04007681 doDown = false;
7682 if (handled) {
7683 return KEY_EVENT_HANDLED;
7684 }
7685 } catch (AbstractMethodError e) {
7686 // onKeyOther was added after 1.0, so if it isn't
7687 // implemented we need to try to dispatch as a regular down.
7688 }
7689 }
7690 if (doDown) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04007691 if (mMovement.onKeyDown(this, mSpannable, keyCode, event)) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04007692 if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) {
7693 mPreventDefaultMovement = true;
7694 }
7695 return KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD;
7696 }
7697 }
7698 // Consume arrows from keyboard devices to prevent focus leaving the editor.
7699 // DPAD/JOY devices (Gamepads, TV remotes) often lack a TAB key so allow those
7700 // to move focus with arrows.
7701 if (event.getSource() == InputDevice.SOURCE_KEYBOARD
7702 && isDirectionalNavigationKey(keyCode)) {
7703 return KEY_EVENT_HANDLED;
7704 }
7705 }
7706
7707 return mPreventDefaultMovement && !KeyEvent.isModifierKey(keyCode)
7708 ? KEY_EVENT_HANDLED : KEY_EVENT_NOT_HANDLED;
7709 }
7710
7711 /**
7712 * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
7713 * can be recorded.
7714 * @hide
7715 */
7716 public void resetErrorChangedFlag() {
7717 /*
7718 * Keep track of what the error was before doing the input
7719 * so that if an input filter changed the error, we leave
7720 * that error showing. Otherwise, we take down whatever
7721 * error was showing when the user types something.
7722 */
7723 if (mEditor != null) mEditor.mErrorWasChanged = false;
7724 }
7725
7726 /**
7727 * @hide
7728 */
7729 public void hideErrorIfUnchanged() {
7730 if (mEditor != null && mEditor.mError != null && !mEditor.mErrorWasChanged) {
7731 setError(null, null);
7732 }
7733 }
7734
7735 @Override
7736 public boolean onKeyUp(int keyCode, KeyEvent event) {
7737 if (!isEnabled()) {
7738 return super.onKeyUp(keyCode, event);
7739 }
7740
7741 if (!KeyEvent.isModifierKey(keyCode)) {
7742 mPreventDefaultMovement = false;
7743 }
7744
7745 switch (keyCode) {
7746 case KeyEvent.KEYCODE_DPAD_CENTER:
7747 if (event.hasNoModifiers()) {
7748 /*
7749 * If there is a click listener, just call through to
7750 * super, which will invoke it.
7751 *
7752 * If there isn't a click listener, try to show the soft
7753 * input method. (It will also
7754 * call performClick(), but that won't do anything in
7755 * this case.)
7756 */
7757 if (!hasOnClickListeners()) {
7758 if (mMovement != null && mText instanceof Editable
7759 && mLayout != null && onCheckIsTextEditor()) {
7760 InputMethodManager imm = InputMethodManager.peekInstance();
7761 viewClicked(imm);
7762 if (imm != null && getShowSoftInputOnFocus()) {
7763 imm.showSoftInput(this, 0);
7764 }
7765 }
7766 }
7767 }
7768 return super.onKeyUp(keyCode, event);
7769
7770 case KeyEvent.KEYCODE_ENTER:
7771 if (event.hasNoModifiers()) {
7772 if (mEditor != null && mEditor.mInputContentType != null
7773 && mEditor.mInputContentType.onEditorActionListener != null
7774 && mEditor.mInputContentType.enterDown) {
7775 mEditor.mInputContentType.enterDown = false;
7776 if (mEditor.mInputContentType.onEditorActionListener.onEditorAction(
7777 this, EditorInfo.IME_NULL, event)) {
7778 return true;
7779 }
7780 }
7781
7782 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
7783 || shouldAdvanceFocusOnEnter()) {
7784 /*
7785 * If there is a click listener, just call through to
7786 * super, which will invoke it.
7787 *
7788 * If there isn't a click listener, try to advance focus,
7789 * but still call through to super, which will reset the
7790 * pressed state and longpress state. (It will also
7791 * call performClick(), but that won't do anything in
7792 * this case.)
7793 */
7794 if (!hasOnClickListeners()) {
7795 View v = focusSearch(FOCUS_DOWN);
7796
7797 if (v != null) {
7798 if (!v.requestFocus(FOCUS_DOWN)) {
7799 throw new IllegalStateException("focus search returned a view "
7800 + "that wasn't able to take focus!");
7801 }
7802
7803 /*
7804 * Return true because we handled the key; super
7805 * will return false because there was no click
7806 * listener.
7807 */
7808 super.onKeyUp(keyCode, event);
7809 return true;
7810 } else if ((event.getFlags()
7811 & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
7812 // No target for next focus, but make sure the IME
7813 // if this came from it.
7814 InputMethodManager imm = InputMethodManager.peekInstance();
7815 if (imm != null && imm.isActive(this)) {
7816 imm.hideSoftInputFromWindow(getWindowToken(), 0);
7817 }
7818 }
7819 }
7820 }
7821 return super.onKeyUp(keyCode, event);
7822 }
7823 break;
7824 }
7825
7826 if (mEditor != null && mEditor.mKeyListener != null) {
7827 if (mEditor.mKeyListener.onKeyUp(this, (Editable) mText, keyCode, event)) {
7828 return true;
7829 }
7830 }
7831
7832 if (mMovement != null && mLayout != null) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04007833 if (mMovement.onKeyUp(this, mSpannable, keyCode, event)) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04007834 return true;
7835 }
7836 }
7837
7838 return super.onKeyUp(keyCode, event);
7839 }
7840
7841 @Override
7842 public boolean onCheckIsTextEditor() {
7843 return mEditor != null && mEditor.mInputType != EditorInfo.TYPE_NULL;
7844 }
7845
7846 @Override
7847 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
7848 if (onCheckIsTextEditor() && isEnabled()) {
7849 mEditor.createInputMethodStateIfNeeded();
7850 outAttrs.inputType = getInputType();
7851 if (mEditor.mInputContentType != null) {
7852 outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
7853 outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
7854 outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
7855 outAttrs.actionId = mEditor.mInputContentType.imeActionId;
7856 outAttrs.extras = mEditor.mInputContentType.extras;
7857 outAttrs.hintLocales = mEditor.mInputContentType.imeHintLocales;
7858 } else {
7859 outAttrs.imeOptions = EditorInfo.IME_NULL;
7860 outAttrs.hintLocales = null;
7861 }
7862 if (focusSearch(FOCUS_DOWN) != null) {
7863 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
7864 }
7865 if (focusSearch(FOCUS_UP) != null) {
7866 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
7867 }
7868 if ((outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION)
7869 == EditorInfo.IME_ACTION_UNSPECIFIED) {
7870 if ((outAttrs.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
7871 // An action has not been set, but the enter key will move to
7872 // the next focus, so set the action to that.
7873 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
7874 } else {
7875 // An action has not been set, and there is no focus to move
7876 // to, so let's just supply a "done" action.
7877 outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
7878 }
7879 if (!shouldAdvanceFocusOnEnter()) {
7880 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
7881 }
7882 }
7883 if (isMultilineInputType(outAttrs.inputType)) {
7884 // Multi-line text editors should always show an enter key.
7885 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
7886 }
7887 outAttrs.hintText = mHint;
7888 if (mText instanceof Editable) {
7889 InputConnection ic = new EditableInputConnection(this);
7890 outAttrs.initialSelStart = getSelectionStart();
7891 outAttrs.initialSelEnd = getSelectionEnd();
7892 outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
7893 return ic;
7894 }
7895 }
7896 return null;
7897 }
7898
7899 /**
7900 * If this TextView contains editable content, extract a portion of it
7901 * based on the information in <var>request</var> in to <var>outText</var>.
7902 * @return Returns true if the text was successfully extracted, else false.
7903 */
7904 public boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
7905 createEditorIfNeeded();
7906 return mEditor.extractText(request, outText);
7907 }
7908
7909 /**
7910 * This is used to remove all style-impacting spans from text before new
7911 * extracted text is being replaced into it, so that we don't have any
7912 * lingering spans applied during the replace.
7913 */
7914 static void removeParcelableSpans(Spannable spannable, int start, int end) {
7915 Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
7916 int i = spans.length;
7917 while (i > 0) {
7918 i--;
7919 spannable.removeSpan(spans[i]);
7920 }
7921 }
7922
7923 /**
7924 * Apply to this text view the given extracted text, as previously
7925 * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
7926 */
7927 public void setExtractedText(ExtractedText text) {
7928 Editable content = getEditableText();
7929 if (text.text != null) {
7930 if (content == null) {
7931 setText(text.text, TextView.BufferType.EDITABLE);
7932 } else {
7933 int start = 0;
7934 int end = content.length();
7935
7936 if (text.partialStartOffset >= 0) {
7937 final int N = content.length();
7938 start = text.partialStartOffset;
7939 if (start > N) start = N;
7940 end = text.partialEndOffset;
7941 if (end > N) end = N;
7942 }
7943
7944 removeParcelableSpans(content, start, end);
7945 if (TextUtils.equals(content.subSequence(start, end), text.text)) {
7946 if (text.text instanceof Spanned) {
7947 // OK to copy spans only.
7948 TextUtils.copySpansFrom((Spanned) text.text, 0, end - start,
7949 Object.class, content, start);
7950 }
7951 } else {
7952 content.replace(start, end, text.text);
7953 }
7954 }
7955 }
7956
7957 // Now set the selection position... make sure it is in range, to
7958 // avoid crashes. If this is a partial update, it is possible that
7959 // the underlying text may have changed, causing us problems here.
7960 // Also we just don't want to trust clients to do the right thing.
7961 Spannable sp = (Spannable) getText();
7962 final int N = sp.length();
7963 int start = text.selectionStart;
7964 if (start < 0) {
7965 start = 0;
7966 } else if (start > N) {
7967 start = N;
7968 }
7969 int end = text.selectionEnd;
7970 if (end < 0) {
7971 end = 0;
7972 } else if (end > N) {
7973 end = N;
7974 }
7975 Selection.setSelection(sp, start, end);
7976
7977 // Finally, update the selection mode.
7978 if ((text.flags & ExtractedText.FLAG_SELECTING) != 0) {
7979 MetaKeyKeyListener.startSelecting(this, sp);
7980 } else {
7981 MetaKeyKeyListener.stopSelecting(this, sp);
7982 }
Justin Klaassenbc81c7a2017-09-18 17:38:50 -04007983
7984 setHintInternal(text.hint);
Justin Klaassen10d07c82017-09-15 17:58:39 -04007985 }
7986
7987 /**
7988 * @hide
7989 */
7990 public void setExtracting(ExtractedTextRequest req) {
7991 if (mEditor.mInputMethodState != null) {
7992 mEditor.mInputMethodState.mExtractedTextRequest = req;
7993 }
7994 // This would stop a possible selection mode, but no such mode is started in case
7995 // extracted mode will start. Some text is selected though, and will trigger an action mode
7996 // in the extracted view.
7997 mEditor.hideCursorAndSpanControllers();
7998 stopTextActionMode();
7999 if (mEditor.mSelectionModifierCursorController != null) {
8000 mEditor.mSelectionModifierCursorController.resetTouchOffsets();
8001 }
8002 }
8003
8004 /**
8005 * Called by the framework in response to a text completion from
8006 * the current input method, provided by it calling
8007 * {@link InputConnection#commitCompletion
8008 * InputConnection.commitCompletion()}. The default implementation does
8009 * nothing; text views that are supporting auto-completion should override
8010 * this to do their desired behavior.
8011 *
8012 * @param text The auto complete text the user has selected.
8013 */
8014 public void onCommitCompletion(CompletionInfo text) {
8015 // intentionally empty
8016 }
8017
8018 /**
8019 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
8020 * dictionary) from the current input method, provided by it calling
8021 * {@link InputConnection#commitCorrection(CorrectionInfo) InputConnection.commitCorrection()}.
8022 * The default implementation flashes the background of the corrected word to provide
8023 * feedback to the user.
8024 *
8025 * @param info The auto correct info about the text that was corrected.
8026 */
8027 public void onCommitCorrection(CorrectionInfo info) {
8028 if (mEditor != null) mEditor.onCommitCorrection(info);
8029 }
8030
8031 public void beginBatchEdit() {
8032 if (mEditor != null) mEditor.beginBatchEdit();
8033 }
8034
8035 public void endBatchEdit() {
8036 if (mEditor != null) mEditor.endBatchEdit();
8037 }
8038
8039 /**
8040 * Called by the framework in response to a request to begin a batch
8041 * of edit operations through a call to link {@link #beginBatchEdit()}.
8042 */
8043 public void onBeginBatchEdit() {
8044 // intentionally empty
8045 }
8046
8047 /**
8048 * Called by the framework in response to a request to end a batch
8049 * of edit operations through a call to link {@link #endBatchEdit}.
8050 */
8051 public void onEndBatchEdit() {
8052 // intentionally empty
8053 }
8054
8055 /**
8056 * Called by the framework in response to a private command from the
8057 * current method, provided by it calling
8058 * {@link InputConnection#performPrivateCommand
8059 * InputConnection.performPrivateCommand()}.
8060 *
8061 * @param action The action name of the command.
8062 * @param data Any additional data for the command. This may be null.
8063 * @return Return true if you handled the command, else false.
8064 */
8065 public boolean onPrivateIMECommand(String action, Bundle data) {
8066 return false;
8067 }
8068
Justin Klaassen4d01eea2018-04-03 23:21:57 -04008069 /** @hide */
8070 @VisibleForTesting
8071 public void nullLayouts() {
Justin Klaassen10d07c82017-09-15 17:58:39 -04008072 if (mLayout instanceof BoringLayout && mSavedLayout == null) {
8073 mSavedLayout = (BoringLayout) mLayout;
8074 }
8075 if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
8076 mSavedHintLayout = (BoringLayout) mHintLayout;
8077 }
8078
8079 mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
8080
8081 mBoring = mHintBoring = null;
8082
8083 // Since it depends on the value of mLayout
8084 if (mEditor != null) mEditor.prepareCursorControllers();
8085 }
8086
8087 /**
8088 * Make a new Layout based on the already-measured size of the view,
8089 * on the assumption that it was measured correctly at some point.
8090 */
8091 private void assumeLayout() {
8092 int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
8093
8094 if (width < 1) {
8095 width = 0;
8096 }
8097
8098 int physicalWidth = width;
8099
8100 if (mHorizontallyScrolling) {
8101 width = VERY_WIDE;
8102 }
8103
8104 makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
8105 physicalWidth, false);
8106 }
8107
8108 private Layout.Alignment getLayoutAlignment() {
8109 Layout.Alignment alignment;
8110 switch (getTextAlignment()) {
8111 case TEXT_ALIGNMENT_GRAVITY:
8112 switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
8113 case Gravity.START:
8114 alignment = Layout.Alignment.ALIGN_NORMAL;
8115 break;
8116 case Gravity.END:
8117 alignment = Layout.Alignment.ALIGN_OPPOSITE;
8118 break;
8119 case Gravity.LEFT:
8120 alignment = Layout.Alignment.ALIGN_LEFT;
8121 break;
8122 case Gravity.RIGHT:
8123 alignment = Layout.Alignment.ALIGN_RIGHT;
8124 break;
8125 case Gravity.CENTER_HORIZONTAL:
8126 alignment = Layout.Alignment.ALIGN_CENTER;
8127 break;
8128 default:
8129 alignment = Layout.Alignment.ALIGN_NORMAL;
8130 break;
8131 }
8132 break;
8133 case TEXT_ALIGNMENT_TEXT_START:
8134 alignment = Layout.Alignment.ALIGN_NORMAL;
8135 break;
8136 case TEXT_ALIGNMENT_TEXT_END:
8137 alignment = Layout.Alignment.ALIGN_OPPOSITE;
8138 break;
8139 case TEXT_ALIGNMENT_CENTER:
8140 alignment = Layout.Alignment.ALIGN_CENTER;
8141 break;
8142 case TEXT_ALIGNMENT_VIEW_START:
8143 alignment = (getLayoutDirection() == LAYOUT_DIRECTION_RTL)
8144 ? Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
8145 break;
8146 case TEXT_ALIGNMENT_VIEW_END:
8147 alignment = (getLayoutDirection() == LAYOUT_DIRECTION_RTL)
8148 ? Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
8149 break;
8150 case TEXT_ALIGNMENT_INHERIT:
8151 // This should never happen as we have already resolved the text alignment
8152 // but better safe than sorry so we just fall through
8153 default:
8154 alignment = Layout.Alignment.ALIGN_NORMAL;
8155 break;
8156 }
8157 return alignment;
8158 }
8159
8160 /**
8161 * The width passed in is now the desired layout width,
8162 * not the full view width with padding.
8163 * {@hide}
8164 */
Justin Klaassen4d01eea2018-04-03 23:21:57 -04008165 @VisibleForTesting
8166 public void makeNewLayout(int wantWidth, int hintWidth,
Justin Klaassen10d07c82017-09-15 17:58:39 -04008167 BoringLayout.Metrics boring,
8168 BoringLayout.Metrics hintBoring,
8169 int ellipsisWidth, boolean bringIntoView) {
8170 stopMarquee();
8171
8172 // Update "old" cached values
8173 mOldMaximum = mMaximum;
8174 mOldMaxMode = mMaxMode;
8175
8176 mHighlightPathBogus = true;
8177
8178 if (wantWidth < 0) {
8179 wantWidth = 0;
8180 }
8181 if (hintWidth < 0) {
8182 hintWidth = 0;
8183 }
8184
8185 Layout.Alignment alignment = getLayoutAlignment();
8186 final boolean testDirChange = mSingleLine && mLayout != null
8187 && (alignment == Layout.Alignment.ALIGN_NORMAL
8188 || alignment == Layout.Alignment.ALIGN_OPPOSITE);
8189 int oldDir = 0;
8190 if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
8191 boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
8192 final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE
8193 && mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
8194 TruncateAt effectiveEllipsize = mEllipsize;
8195 if (mEllipsize == TruncateAt.MARQUEE
8196 && mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
8197 effectiveEllipsize = TruncateAt.END_SMALL;
8198 }
8199
8200 if (mTextDir == null) {
8201 mTextDir = getTextDirectionHeuristic();
8202 }
8203
8204 mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
8205 effectiveEllipsize, effectiveEllipsize == mEllipsize);
8206 if (switchEllipsize) {
8207 TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE
8208 ? TruncateAt.END : TruncateAt.MARQUEE;
8209 mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
8210 shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
8211 }
8212
8213 shouldEllipsize = mEllipsize != null;
8214 mHintLayout = null;
8215
8216 if (mHint != null) {
8217 if (shouldEllipsize) hintWidth = wantWidth;
8218
8219 if (hintBoring == UNKNOWN_BORING) {
8220 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
8221 mHintBoring);
8222 if (hintBoring != null) {
8223 mHintBoring = hintBoring;
8224 }
8225 }
8226
8227 if (hintBoring != null) {
8228 if (hintBoring.width <= hintWidth
8229 && (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
8230 if (mSavedHintLayout != null) {
8231 mHintLayout = mSavedHintLayout.replaceOrMake(mHint, mTextPaint,
8232 hintWidth, alignment, mSpacingMult, mSpacingAdd,
8233 hintBoring, mIncludePad);
8234 } else {
8235 mHintLayout = BoringLayout.make(mHint, mTextPaint,
8236 hintWidth, alignment, mSpacingMult, mSpacingAdd,
8237 hintBoring, mIncludePad);
8238 }
8239
8240 mSavedHintLayout = (BoringLayout) mHintLayout;
8241 } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
8242 if (mSavedHintLayout != null) {
8243 mHintLayout = mSavedHintLayout.replaceOrMake(mHint, mTextPaint,
8244 hintWidth, alignment, mSpacingMult, mSpacingAdd,
8245 hintBoring, mIncludePad, mEllipsize,
8246 ellipsisWidth);
8247 } else {
8248 mHintLayout = BoringLayout.make(mHint, mTextPaint,
8249 hintWidth, alignment, mSpacingMult, mSpacingAdd,
8250 hintBoring, mIncludePad, mEllipsize,
8251 ellipsisWidth);
8252 }
8253 }
8254 }
8255 // TODO: code duplication with makeSingleLayout()
8256 if (mHintLayout == null) {
8257 StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
8258 mHint.length(), mTextPaint, hintWidth)
8259 .setAlignment(alignment)
8260 .setTextDirection(mTextDir)
8261 .setLineSpacing(mSpacingAdd, mSpacingMult)
8262 .setIncludePad(mIncludePad)
8263 .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
8264 .setBreakStrategy(mBreakStrategy)
8265 .setHyphenationFrequency(mHyphenationFrequency)
8266 .setJustificationMode(mJustificationMode)
8267 .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
8268 if (shouldEllipsize) {
8269 builder.setEllipsize(mEllipsize)
8270 .setEllipsizedWidth(ellipsisWidth);
8271 }
8272 mHintLayout = builder.build();
8273 }
8274 }
8275
8276 if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
8277 registerForPreDraw();
8278 }
8279
8280 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
8281 if (!compressText(ellipsisWidth)) {
8282 final int height = mLayoutParams.height;
8283 // If the size of the view does not depend on the size of the text, try to
8284 // start the marquee immediately
8285 if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
8286 startMarquee();
8287 } else {
8288 // Defer the start of the marquee until we know our width (see setFrame())
8289 mRestartMarquee = true;
8290 }
8291 }
8292 }
8293
8294 // CursorControllers need a non-null mLayout
8295 if (mEditor != null) mEditor.prepareCursorControllers();
8296 }
8297
8298 /**
Justin Klaassen4d01eea2018-04-03 23:21:57 -04008299 * Returns true if DynamicLayout is required
8300 *
8301 * @hide
8302 */
8303 @VisibleForTesting
8304 public boolean useDynamicLayout() {
8305 return isTextSelectable() || (mSpannable != null && mPrecomputed == null);
8306 }
8307
8308 /**
Justin Klaassen10d07c82017-09-15 17:58:39 -04008309 * @hide
8310 */
8311 protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
8312 Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
8313 boolean useSaved) {
8314 Layout result = null;
Justin Klaassen4d01eea2018-04-03 23:21:57 -04008315 if (useDynamicLayout()) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04008316 final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint,
8317 wantWidth)
8318 .setDisplayText(mTransformed)
8319 .setAlignment(alignment)
8320 .setTextDirection(mTextDir)
8321 .setLineSpacing(mSpacingAdd, mSpacingMult)
8322 .setIncludePad(mIncludePad)
8323 .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
8324 .setBreakStrategy(mBreakStrategy)
8325 .setHyphenationFrequency(mHyphenationFrequency)
8326 .setJustificationMode(mJustificationMode)
8327 .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
8328 .setEllipsizedWidth(ellipsisWidth);
8329 result = builder.build();
8330 } else {
8331 if (boring == UNKNOWN_BORING) {
8332 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
8333 if (boring != null) {
8334 mBoring = boring;
8335 }
8336 }
8337
8338 if (boring != null) {
8339 if (boring.width <= wantWidth
8340 && (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
8341 if (useSaved && mSavedLayout != null) {
8342 result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
8343 wantWidth, alignment, mSpacingMult, mSpacingAdd,
8344 boring, mIncludePad);
8345 } else {
8346 result = BoringLayout.make(mTransformed, mTextPaint,
8347 wantWidth, alignment, mSpacingMult, mSpacingAdd,
8348 boring, mIncludePad);
8349 }
8350
8351 if (useSaved) {
8352 mSavedLayout = (BoringLayout) result;
8353 }
8354 } else if (shouldEllipsize && boring.width <= wantWidth) {
8355 if (useSaved && mSavedLayout != null) {
8356 result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
8357 wantWidth, alignment, mSpacingMult, mSpacingAdd,
8358 boring, mIncludePad, effectiveEllipsize,
8359 ellipsisWidth);
8360 } else {
8361 result = BoringLayout.make(mTransformed, mTextPaint,
8362 wantWidth, alignment, mSpacingMult, mSpacingAdd,
8363 boring, mIncludePad, effectiveEllipsize,
8364 ellipsisWidth);
8365 }
8366 }
8367 }
8368 }
8369 if (result == null) {
8370 StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
8371 0, mTransformed.length(), mTextPaint, wantWidth)
8372 .setAlignment(alignment)
8373 .setTextDirection(mTextDir)
8374 .setLineSpacing(mSpacingAdd, mSpacingMult)
8375 .setIncludePad(mIncludePad)
8376 .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
8377 .setBreakStrategy(mBreakStrategy)
8378 .setHyphenationFrequency(mHyphenationFrequency)
8379 .setJustificationMode(mJustificationMode)
8380 .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
8381 if (shouldEllipsize) {
8382 builder.setEllipsize(effectiveEllipsize)
8383 .setEllipsizedWidth(ellipsisWidth);
8384 }
8385 result = builder.build();
8386 }
8387 return result;
8388 }
8389
8390 private boolean compressText(float width) {
8391 if (isHardwareAccelerated()) return false;
8392
8393 // Only compress the text if it hasn't been compressed by the previous pass
8394 if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX
8395 && mTextPaint.getTextScaleX() == 1.0f) {
8396 final float textWidth = mLayout.getLineWidth(0);
8397 final float overflow = (textWidth + 1.0f - width) / width;
8398 if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
8399 mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
8400 post(new Runnable() {
8401 public void run() {
8402 requestLayout();
8403 }
8404 });
8405 return true;
8406 }
8407 }
8408
8409 return false;
8410 }
8411
8412 private static int desired(Layout layout) {
8413 int n = layout.getLineCount();
8414 CharSequence text = layout.getText();
8415 float max = 0;
8416
8417 // if any line was wrapped, we can't use it.
8418 // but it's ok for the last line not to have a newline
8419
8420 for (int i = 0; i < n - 1; i++) {
8421 if (text.charAt(layout.getLineEnd(i) - 1) != '\n') {
8422 return -1;
8423 }
8424 }
8425
8426 for (int i = 0; i < n; i++) {
8427 max = Math.max(max, layout.getLineWidth(i));
8428 }
8429
8430 return (int) Math.ceil(max);
8431 }
8432
8433 /**
8434 * Set whether the TextView includes extra top and bottom padding to make
8435 * room for accents that go above the normal ascent and descent.
8436 * The default is true.
8437 *
8438 * @see #getIncludeFontPadding()
8439 *
8440 * @attr ref android.R.styleable#TextView_includeFontPadding
8441 */
8442 public void setIncludeFontPadding(boolean includepad) {
8443 if (mIncludePad != includepad) {
8444 mIncludePad = includepad;
8445
8446 if (mLayout != null) {
8447 nullLayouts();
8448 requestLayout();
8449 invalidate();
8450 }
8451 }
8452 }
8453
8454 /**
8455 * Gets whether the TextView includes extra top and bottom padding to make
8456 * room for accents that go above the normal ascent and descent.
8457 *
8458 * @see #setIncludeFontPadding(boolean)
8459 *
8460 * @attr ref android.R.styleable#TextView_includeFontPadding
8461 */
8462 public boolean getIncludeFontPadding() {
8463 return mIncludePad;
8464 }
8465
Justin Klaassen4d01eea2018-04-03 23:21:57 -04008466 /** @hide */
8467 @VisibleForTesting
8468 public static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
Justin Klaassen10d07c82017-09-15 17:58:39 -04008469
8470 @Override
8471 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
8472 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
8473 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
8474 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
8475 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
8476
8477 int width;
8478 int height;
8479
8480 BoringLayout.Metrics boring = UNKNOWN_BORING;
8481 BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
8482
8483 if (mTextDir == null) {
8484 mTextDir = getTextDirectionHeuristic();
8485 }
8486
8487 int des = -1;
8488 boolean fromexisting = false;
8489 final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
8490 ? (float) widthSize : Float.MAX_VALUE;
8491
8492 if (widthMode == MeasureSpec.EXACTLY) {
8493 // Parent has told us how big to be. So be it.
8494 width = widthSize;
8495 } else {
8496 if (mLayout != null && mEllipsize == null) {
8497 des = desired(mLayout);
8498 }
8499
8500 if (des < 0) {
8501 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
8502 if (boring != null) {
8503 mBoring = boring;
8504 }
8505 } else {
8506 fromexisting = true;
8507 }
8508
8509 if (boring == null || boring == UNKNOWN_BORING) {
8510 if (des < 0) {
8511 des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
8512 mTransformed.length(), mTextPaint, mTextDir, widthLimit));
8513 }
8514 width = des;
8515 } else {
8516 width = boring.width;
8517 }
8518
8519 final Drawables dr = mDrawables;
8520 if (dr != null) {
8521 width = Math.max(width, dr.mDrawableWidthTop);
8522 width = Math.max(width, dr.mDrawableWidthBottom);
8523 }
8524
8525 if (mHint != null) {
8526 int hintDes = -1;
8527 int hintWidth;
8528
8529 if (mHintLayout != null && mEllipsize == null) {
8530 hintDes = desired(mHintLayout);
8531 }
8532
8533 if (hintDes < 0) {
8534 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
8535 if (hintBoring != null) {
8536 mHintBoring = hintBoring;
8537 }
8538 }
8539
8540 if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
8541 if (hintDes < 0) {
8542 hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
8543 mHint.length(), mTextPaint, mTextDir, widthLimit));
8544 }
8545 hintWidth = hintDes;
8546 } else {
8547 hintWidth = hintBoring.width;
8548 }
8549
8550 if (hintWidth > width) {
8551 width = hintWidth;
8552 }
8553 }
8554
8555 width += getCompoundPaddingLeft() + getCompoundPaddingRight();
8556
8557 if (mMaxWidthMode == EMS) {
8558 width = Math.min(width, mMaxWidth * getLineHeight());
8559 } else {
8560 width = Math.min(width, mMaxWidth);
8561 }
8562
8563 if (mMinWidthMode == EMS) {
8564 width = Math.max(width, mMinWidth * getLineHeight());
8565 } else {
8566 width = Math.max(width, mMinWidth);
8567 }
8568
8569 // Check against our minimum width
8570 width = Math.max(width, getSuggestedMinimumWidth());
8571
8572 if (widthMode == MeasureSpec.AT_MOST) {
8573 width = Math.min(widthSize, width);
8574 }
8575 }
8576
8577 int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
8578 int unpaddedWidth = want;
8579
8580 if (mHorizontallyScrolling) want = VERY_WIDE;
8581
8582 int hintWant = want;
8583 int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
8584
8585 if (mLayout == null) {
8586 makeNewLayout(want, hintWant, boring, hintBoring,
8587 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
8588 } else {
8589 final boolean layoutChanged = (mLayout.getWidth() != want) || (hintWidth != hintWant)
8590 || (mLayout.getEllipsizedWidth()
8591 != width - getCompoundPaddingLeft() - getCompoundPaddingRight());
8592
8593 final boolean widthChanged = (mHint == null) && (mEllipsize == null)
8594 && (want > mLayout.getWidth())
8595 && (mLayout instanceof BoringLayout
8596 || (fromexisting && des >= 0 && des <= want));
8597
8598 final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
8599
8600 if (layoutChanged || maximumChanged) {
8601 if (!maximumChanged && widthChanged) {
8602 mLayout.increaseWidthTo(want);
8603 } else {
8604 makeNewLayout(want, hintWant, boring, hintBoring,
8605 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
8606 }
8607 } else {
8608 // Nothing has changed
8609 }
8610 }
8611
8612 if (heightMode == MeasureSpec.EXACTLY) {
8613 // Parent has told us how big to be. So be it.
8614 height = heightSize;
8615 mDesiredHeightAtMeasure = -1;
8616 } else {
8617 int desired = getDesiredHeight();
8618
8619 height = desired;
8620 mDesiredHeightAtMeasure = desired;
8621
8622 if (heightMode == MeasureSpec.AT_MOST) {
8623 height = Math.min(desired, heightSize);
8624 }
8625 }
8626
8627 int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
8628 if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
8629 unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
8630 }
8631
8632 /*
8633 * We didn't let makeNewLayout() register to bring the cursor into view,
8634 * so do it here if there is any possibility that it is needed.
8635 */
8636 if (mMovement != null
8637 || mLayout.getWidth() > unpaddedWidth
8638 || mLayout.getHeight() > unpaddedHeight) {
8639 registerForPreDraw();
8640 } else {
8641 scrollTo(0, 0);
8642 }
8643
8644 setMeasuredDimension(width, height);
8645 }
8646
8647 /**
8648 * Automatically computes and sets the text size.
8649 */
8650 private void autoSizeText() {
8651 if (!isAutoSizeEnabled()) {
8652 return;
8653 }
8654
8655 if (mNeedsAutoSizeText) {
8656 if (getMeasuredWidth() <= 0 || getMeasuredHeight() <= 0) {
8657 return;
8658 }
8659
8660 final int availableWidth = mHorizontallyScrolling
8661 ? VERY_WIDE
8662 : getMeasuredWidth() - getTotalPaddingLeft() - getTotalPaddingRight();
8663 final int availableHeight = getMeasuredHeight() - getExtendedPaddingBottom()
8664 - getExtendedPaddingTop();
8665
8666 if (availableWidth <= 0 || availableHeight <= 0) {
8667 return;
8668 }
8669
8670 synchronized (TEMP_RECTF) {
8671 TEMP_RECTF.setEmpty();
8672 TEMP_RECTF.right = availableWidth;
8673 TEMP_RECTF.bottom = availableHeight;
8674 final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
8675
8676 if (optimalTextSize != getTextSize()) {
8677 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize,
8678 false /* shouldRequestLayout */);
8679
8680 makeNewLayout(availableWidth, 0 /* hintWidth */, UNKNOWN_BORING, UNKNOWN_BORING,
8681 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
8682 false /* bringIntoView */);
8683 }
8684 }
8685 }
8686 // Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing
8687 // after the next layout pass should set this to false.
8688 mNeedsAutoSizeText = true;
8689 }
8690
8691 /**
8692 * Performs a binary search to find the largest text size that will still fit within the size
8693 * available to this view.
8694 */
8695 private int findLargestTextSizeWhichFits(RectF availableSpace) {
8696 final int sizesCount = mAutoSizeTextSizesInPx.length;
8697 if (sizesCount == 0) {
8698 throw new IllegalStateException("No available text sizes to choose from.");
8699 }
8700
8701 int bestSizeIndex = 0;
8702 int lowIndex = bestSizeIndex + 1;
8703 int highIndex = sizesCount - 1;
8704 int sizeToTryIndex;
8705 while (lowIndex <= highIndex) {
8706 sizeToTryIndex = (lowIndex + highIndex) / 2;
8707 if (suggestedSizeFitsInSpace(mAutoSizeTextSizesInPx[sizeToTryIndex], availableSpace)) {
8708 bestSizeIndex = lowIndex;
8709 lowIndex = sizeToTryIndex + 1;
8710 } else {
8711 highIndex = sizeToTryIndex - 1;
8712 bestSizeIndex = highIndex;
8713 }
8714 }
8715
8716 return mAutoSizeTextSizesInPx[bestSizeIndex];
8717 }
8718
8719 private boolean suggestedSizeFitsInSpace(int suggestedSizeInPx, RectF availableSpace) {
8720 final CharSequence text = mTransformed != null
8721 ? mTransformed
8722 : getText();
8723 final int maxLines = getMaxLines();
8724 if (mTempTextPaint == null) {
8725 mTempTextPaint = new TextPaint();
8726 } else {
8727 mTempTextPaint.reset();
8728 }
8729 mTempTextPaint.set(getPaint());
8730 mTempTextPaint.setTextSize(suggestedSizeInPx);
8731
8732 final StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(
8733 text, 0, text.length(), mTempTextPaint, Math.round(availableSpace.right));
8734
8735 layoutBuilder.setAlignment(getLayoutAlignment())
8736 .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
8737 .setIncludePad(getIncludeFontPadding())
8738 .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
8739 .setBreakStrategy(getBreakStrategy())
8740 .setHyphenationFrequency(getHyphenationFrequency())
8741 .setJustificationMode(getJustificationMode())
8742 .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
8743 .setTextDirection(getTextDirectionHeuristic());
8744
8745 final StaticLayout layout = layoutBuilder.build();
8746
8747 // Lines overflow.
8748 if (maxLines != -1 && layout.getLineCount() > maxLines) {
8749 return false;
8750 }
8751
8752 // Height overflow.
8753 if (layout.getHeight() > availableSpace.bottom) {
8754 return false;
8755 }
8756
8757 return true;
8758 }
8759
8760 private int getDesiredHeight() {
8761 return Math.max(
8762 getDesiredHeight(mLayout, true),
8763 getDesiredHeight(mHintLayout, mEllipsize != null));
8764 }
8765
8766 private int getDesiredHeight(Layout layout, boolean cap) {
8767 if (layout == null) {
8768 return 0;
8769 }
8770
8771 /*
8772 * Don't cap the hint to a certain number of lines.
8773 * (Do cap it, though, if we have a maximum pixel height.)
8774 */
8775 int desired = layout.getHeight(cap);
8776
8777 final Drawables dr = mDrawables;
8778 if (dr != null) {
8779 desired = Math.max(desired, dr.mDrawableHeightLeft);
8780 desired = Math.max(desired, dr.mDrawableHeightRight);
8781 }
8782
8783 int linecount = layout.getLineCount();
8784 final int padding = getCompoundPaddingTop() + getCompoundPaddingBottom();
8785 desired += padding;
8786
8787 if (mMaxMode != LINES) {
8788 desired = Math.min(desired, mMaximum);
Justin Klaassenbc81c7a2017-09-18 17:38:50 -04008789 } else if (cap && linecount > mMaximum && (layout instanceof DynamicLayout
8790 || layout instanceof BoringLayout)) {
Justin Klaassen10d07c82017-09-15 17:58:39 -04008791 desired = layout.getLineTop(mMaximum);
8792
8793 if (dr != null) {
8794 desired = Math.max(desired, dr.mDrawableHeightLeft);
8795 desired = Math.max(desired, dr.mDrawableHeightRight);
8796 }
8797
8798 desired += padding;
8799 linecount = mMaximum;
8800 }
8801
8802 if (mMinMode == LINES) {
8803 if (linecount < mMinimum) {
8804 desired += getLineHeight() * (mMinimum - linecount);
8805 }
8806 } else {
8807 desired = Math.max(desired, mMinimum);
8808 }
8809
8810 // Check against our minimum height
8811 desired = Math.max(desired, getSuggestedMinimumHeight());
8812
8813 return desired;
8814 }
8815
8816 /**
8817 * Check whether a change to the existing text layout requires a
8818 * new view layout.
8819 */
8820 private void checkForResize() {
8821 boolean sizeChanged = false;
8822
8823 if (mLayout != null) {
8824 // Check if our width changed
8825 if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
8826 sizeChanged = true;
8827 invalidate();
8828 }
8829
8830 // Check if our height changed
8831 if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
8832 int desiredHeight = getDesiredHeight();
8833
8834 if (desiredHeight != this.getHeight()) {
8835 sizeChanged = true;
8836 }
8837 } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
8838 if (mDesiredHeightAtMeasure >= 0) {
8839 int desiredHeight = getDesiredHeight();
8840
8841 if (desiredHeight != mDesiredHeightAtMeasure) {
8842 sizeChanged = true;
8843 }
8844 }
8845 }
8846 }
8847
8848 if (sizeChanged) {
8849 requestLayout();
8850 // caller will have already invalidated
8851 }
8852 }
8853
8854 /**
8855 * Check whether entirely new text requires a new view layout
8856 * or merely a new text layout.
8857 */
8858 private void checkForRelayout() {
8859 // If we have a fixed width, we can just swap in a new text layout
8860 // if the text height stays the same or if the view height is fixed.
8861
8862 if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
8863 || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
8864 && (mHint == null || mHintLayout != null)
8865 && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
8866 // Static width, so try making a new text layout.
8867
8868 int oldht = mLayout.getHeight();
8869 int want = mLayout.getWidth();
8870 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
8871
8872 /*
8873 * No need to bring the text into view, since the size is not
8874 * changing (unless we do the requestLayout(), in which case it
8875 * will happen at measure).
8876 */
8877 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
8878 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
8879 false);
8880
8881 if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
8882 // In a fixed-height view, so use our new text layout.
8883 if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
8884 && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
8885 autoSizeText();
8886 invalidate();
8887 return;
8888 }
8889
8890 // Dynamic height, but height has stayed the same,
8891 // so use our new text layout.
8892 if (mLayout.getHeight() == oldht
8893 && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
8894 autoSizeText();
8895 invalidate();
8896 return;
8897 }
8898 }
8899
8900 // We lose: the height has changed and we have a dynamic height.
8901 // Request a new view layout using our new text layout.
8902 requestLayout();
8903 invalidate();
8904 } else {
8905 // Dynamic width, so we have no choice but to request a new
8906 // view layout with a new text layout.
8907 nullLayouts();
8908 requestLayout();
8909 invalidate();
8910 }
8911 }
8912
8913 @Override
8914 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
8915 super.onLayout(changed, left, top, right, bottom);
8916 if (mDeferScroll >= 0) {
8917 int curs = mDeferScroll;
8918 mDeferScroll = -1;
8919 bringPointIntoView(Math.min(curs, mText.length()));
8920 }
8921 // Call auto-size after the width and height have been calculated.
8922 autoSizeText();
8923 }
8924
8925 private boolean isShowingHint() {
8926 return TextUtils.isEmpty(mText) && !TextUtils.isEmpty(mHint);
8927 }
8928
8929 /**
8930 * Returns true if anything changed.
8931 */
8932 private boolean bringTextIntoView() {
8933 Layout layout = isShowingHint() ? mHintLayout : mLayout;
8934 int line = 0;
8935 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
8936 line = layout.getLineCount() - 1;
8937 }
8938
8939 Layout.Alignment a = layout.getParagraphAlignment(line);
8940 int dir = layout.getParagraphDirection(line);
8941 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
8942 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
8943 int ht = layout.getHeight();
8944
8945 int scrollx, scrolly;
8946
8947 // Convert to left, center, or right alignment.
8948 if (a == Layout.Alignment.ALIGN_NORMAL) {
8949 a = dir == Layout.DIR_LEFT_TO_RIGHT
8950 ? Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
8951 } else if (a == Layout.Alignment.ALIGN_OPPOSITE) {
8952 a = dir == Layout.DIR_LEFT_TO_RIGHT
8953 ? Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
8954 }
8955
8956 if (a == Layout.Alignment.ALIGN_CENTER) {
8957 /*
8958 * Keep centered if possible, or, if it is too wide to fit,
8959 * keep leading edge in view.
8960 */
8961
8962 int left = (int) Math.floor(layout.getLineLeft(line));
8963 int right = (int) Math.ceil(layout.getLineRight(line));
8964
8965 if (right - left < hspace) {
8966 scrollx = (right + left) / 2 - hspace / 2;
8967 } else {
8968 if (dir < 0) {
8969 scrollx = right - hspace;
8970 } else {
8971 scrollx = left;
8972 }
8973 }
8974 } else if (a == Layout.Alignment.ALIGN_RIGHT) {
8975 int right = (int) Math.ceil(layout.getLineRight(line));
8976 scrollx = right - hspace;
8977 } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
8978 scrollx = (int) Math.floor(layout.getLineLeft(line));
8979 }
8980
8981 if (ht < vspace) {
8982 scrolly = 0;
8983 } else {
8984 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
8985 scrolly = ht - vspace;
8986 } else {
8987 scrolly = 0;
8988 }
8989 }
8990
8991 if (scrollx != mScrollX || scrolly != mScrollY) {
8992 scrollTo(scrollx, scrolly);
8993 return true;
8994 } else {
8995 return false;
8996 }
8997 }
8998
8999 /**
9000 * Move the point, specified by the offset, into the view if it is needed.
9001 * This has to be called after layout. Returns true if anything changed.
9002 */
9003 public boolean bringPointIntoView(int offset) {
9004 if (isLayoutRequested()) {
9005 mDeferScroll = offset;
9006 return false;
9007 }
9008 boolean changed = false;
9009
9010 Layout layout = isShowingHint() ? mHintLayout : mLayout;
9011
9012 if (layout == null) return changed;
9013
9014 int line = layout.getLineForOffset(offset);
9015
9016 int grav;
9017
9018 switch (layout.getParagraphAlignment(line)) {
9019 case ALIGN_LEFT:
9020 grav = 1;
9021 break;
9022 case ALIGN_RIGHT:
9023 grav = -1;
9024 break;
9025 case ALIGN_NORMAL:
9026 grav = layout.getParagraphDirection(line);
9027 break;
9028 case ALIGN_OPPOSITE:
9029 grav = -layout.getParagraphDirection(line);
9030 break;
9031 case ALIGN_CENTER:
9032 default:
9033 grav = 0;
9034 break;
9035 }
9036
9037 // We only want to clamp the cursor to fit within the layout width
9038 // in left-to-right modes, because in a right to left alignment,
9039 // we want to scroll to keep the line-right on the screen, as other
9040 // lines are likely to have text flush with the right margin, which
9041 // we want to keep visible.
9042 // A better long-term solution would probably be to measure both
9043 // the full line and a blank-trimmed version, and, for example, use
9044 // the latter measurement for centering and right alignment, but for
9045 // the time being we only implement the cursor clamping in left to
9046 // right where it is most likely to be annoying.
9047 final boolean clamped = grav > 0;
9048 // FIXME: Is it okay to truncate this, or should we round?
9049 final int x = (int) layout.getPrimaryHorizontal(offset, clamped);
9050 final int top = layout.getLineTop(line);
9051 final int bottom = layout.getLineTop(line + 1);
9052
9053 int left = (int) Math.floor(layout.getLineLeft(line));
9054 int right = (int) Math.ceil(layout.getLineRight(line));
9055 int ht = layout.getHeight();
9056
9057 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
9058 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
9059 if (!mHorizontallyScrolling && right - left > hspace && right > x) {
9060 // If cursor has been clamped, make sure we don't scroll.
9061 right = Math.max(x, left + hspace);
9062 }
9063
9064 int hslack = (bottom - top) / 2;
9065 int vslack = hslack;
9066
9067 if (vslack > vspace / 4) {
9068 vslack = vspace / 4;
9069 }
9070 if (hslack > hspace / 4) {
9071 hslack = hspace / 4;
9072 }
9073
9074 int hs = mScrollX;
9075 int vs = mScrollY;
9076
9077 if (top - vs < vslack) {
9078 vs = top - vslack;
9079 }
9080 if (bottom - vs > vspace - vslack) {
9081 vs = bottom - (vspace - vslack);
9082 }
9083 if (ht - vs < vspace) {
9084 vs = ht - vspace;
9085 }
9086 if (0 - vs > 0) {
9087 vs = 0;
9088 }
9089
9090 if (grav != 0) {
9091 if (x - hs < hslack) {
9092 hs = x - hslack;
9093 }
9094 if (x - hs > hspace - hslack) {
9095 hs = x - (hspace - hslack);
9096 }
9097 }
9098
9099 if (grav < 0) {
9100 if (left - hs > 0) {
9101 hs = left;
9102 }
9103 if (right - hs < hspace) {
9104 hs = right - hspace;
9105 }
9106 } else if (grav > 0) {
9107 if (right - hs < hspace) {
9108 hs = right - hspace;
9109 }
9110 if (left - hs > 0) {
9111 hs = left;
9112 }
9113 } else /* grav == 0 */ {
9114 if (right - left <= hspace) {
9115 /*
9116 * If the entire text fits, center it exactly.
9117 */
9118 hs = left - (hspace - (right - left)) / 2;
9119 } else if (x > right - hslack) {
9120 /*
9121 * If we are near the right edge, keep the right edge
9122 * at the edge of the view.
9123 */
9124 hs = right - hspace;
9125 } else if (x < left + hslack) {
9126 /*
9127 * If we are near the left edge, keep the left edge
9128 * at the edge of the view.
9129 */
9130 hs = left;
9131 } else if (left > hs) {
9132 /*
9133 * Is there whitespace visible at the left? Fix it if so.
9134 */
9135 hs = left;
9136 } else if (right < hs + hspace) {
9137 /*
9138 * Is there whitespace visible at the right? Fix it if so.
9139 */
9140 hs = right - hspace;
9141 } else {
9142 /*
9143 * Otherwise, float as needed.
9144 */
9145 if (x - hs < hslack) {
9146 hs = x - hslack;
9147 }
9148 if (x - hs > hspace - hslack) {
9149 hs = x - (hspace - hslack);
9150 }
9151 }
9152 }
9153
9154 if (hs != mScrollX || vs != mScrollY) {
9155 if (mScroller == null) {
9156 scrollTo(hs, vs);
9157 } else {
9158 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
9159 int dx = hs - mScrollX;
9160 int dy = vs - mScrollY;
9161
9162 if (duration > ANIMATED_SCROLL_GAP) {
9163 mScroller.startScroll(mScrollX, mScrollY, dx, dy);
9164 awakenScrollBars(mScroller.getDuration());
9165 invalidate();
9166 } else {
9167 if (!mScroller.isFinished()) {
9168 mScroller.abortAnimation();
9169 }
9170
9171 scrollBy(dx, dy);
9172 }
9173
9174 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
9175 }
9176
9177 changed = true;
9178 }
9179
9180 if (isFocused()) {
9181 // This offsets because getInterestingRect() is in terms of viewport coordinates, but
9182 // requestRectangleOnScreen() is in terms of content coordinates.
9183
9184 // The offsets here are to ensure the rectangle we are using is
9185 // within our view bounds, in case the cursor is on the far left
9186 // or right. If it isn't withing the bounds, then this request
9187 // will be ignored.
9188 if (mTempRect == null) mTempRect = new Rect();
9189 mTempRect.set(x - 2, top, x + 2, bottom);
9190 getInterestingRect(mTempRect, line);
9191 mTempRect.offset(mScrollX, mScrollY);
9192
9193 if (requestRectangleOnScreen(mTempRect)) {
9194 changed = true;
9195 }
9196 }
9197
9198 return changed;
9199 }
9200
9201 /**
9202 * Move the cursor, if needed, so that it is at an offset that is visible
9203 * to the user. This will not move the cursor if it represents more than
9204 * one character (a selection range). This will only work if the
9205 * TextView contains spannable text; otherwise it will do nothing.
9206 *
9207 * @return True if the cursor was actually moved, false otherwise.
9208 */
9209 public boolean moveCursorToVisibleOffset() {
9210 if (!(mText instanceof Spannable)) {
9211 return false;
9212 }
9213 int start = getSelectionStart();
9214 int end = getSelectionEnd();
9215 if (start != end) {
9216 return false;
9217 }
9218
9219 // First: make sure the line is visible on screen:
9220
9221 int line = mLayout.getLineForOffset(start);
9222
9223 final int top = mLayout.getLineTop(line);
9224 final int bottom = mLayout.getLineTop(line + 1);
9225 final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
9226 int vslack = (bottom - top) / 2;
9227 if (vslack > vspace / 4) {
9228 vslack = vspace / 4;
9229 }
9230 final int vs = mScrollY;
9231
9232 if (top < (vs + vslack)) {
9233 line = mLayout.getLineForVertical(vs + vslack + (bottom - top));
9234 } else if (bottom > (vspace + vs - vslack)) {
9235 line = mLayout.getLineForVertical(vspace + vs - vslack - (bottom - top));
9236 }
9237
9238 // Next: make sure the character is visible on screen:
9239
9240 final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
9241 final int hs = mScrollX;
9242 final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
9243 final int rightChar = mLayout.getOffsetForHorizontal(line, hspace + hs);
9244
9245 // line might contain bidirectional text
9246 final int lowChar = leftChar < rightChar ? leftChar : rightChar;
9247 final int highChar = leftChar > rightChar ? leftChar : rightChar;
9248
9249 int newStart = start;
9250 if (newStart < lowChar) {
9251 newStart = lowChar;
9252 } else if (newStart > highChar) {
9253 newStart = highChar;
9254 }
9255
9256 if (newStart != start) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04009257 Selection.setSelection(mSpannable, newStart);
Justin Klaassen10d07c82017-09-15 17:58:39 -04009258 return true;
9259 }
9260
9261 return false;
9262 }
9263
9264 @Override
9265 public void computeScroll() {
9266 if (mScroller != null) {
9267 if (mScroller.computeScrollOffset()) {
9268 mScrollX = mScroller.getCurrX();
9269 mScrollY = mScroller.getCurrY();
9270 invalidateParentCaches();
9271 postInvalidate(); // So we draw again
9272 }
9273 }
9274 }
9275
9276 private void getInterestingRect(Rect r, int line) {
9277 convertFromViewportToContentCoordinates(r);
9278
9279 // Rectangle can can be expanded on first and last line to take
9280 // padding into account.
9281 // TODO Take left/right padding into account too?
9282 if (line == 0) r.top -= getExtendedPaddingTop();
9283 if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
9284 }
9285
9286 private void convertFromViewportToContentCoordinates(Rect r) {
9287 final int horizontalOffset = viewportToContentHorizontalOffset();
9288 r.left += horizontalOffset;
9289 r.right += horizontalOffset;
9290
9291 final int verticalOffset = viewportToContentVerticalOffset();
9292 r.top += verticalOffset;
9293 r.bottom += verticalOffset;
9294 }
9295
9296 int viewportToContentHorizontalOffset() {
9297 return getCompoundPaddingLeft() - mScrollX;
9298 }
9299
9300 int viewportToContentVerticalOffset() {
9301 int offset = getExtendedPaddingTop() - mScrollY;
9302 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
9303 offset += getVerticalOffset(false);
9304 }
9305 return offset;
9306 }
9307
9308 @Override
9309 public void debug(int depth) {
9310 super.debug(depth);
9311
9312 String output = debugIndent(depth);
9313 output += "frame={" + mLeft + ", " + mTop + ", " + mRight
9314 + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
9315 + "} ";
9316
9317 if (mText != null) {
9318
9319 output += "mText=\"" + mText + "\" ";
9320 if (mLayout != null) {
9321 output += "mLayout width=" + mLayout.getWidth()
9322 + " height=" + mLayout.getHeight();
9323 }
9324 } else {
9325 output += "mText=NULL";
9326 }
9327 Log.d(VIEW_LOG_TAG, output);
9328 }
9329
9330 /**
9331 * Convenience for {@link Selection#getSelectionStart}.
9332 */
9333 @ViewDebug.ExportedProperty(category = "text")
9334 public int getSelectionStart() {
9335 return Selection.getSelectionStart(getText());
9336 }
9337
9338 /**
9339 * Convenience for {@link Selection#getSelectionEnd}.
9340 */
9341 @ViewDebug.ExportedProperty(category = "text")
9342 public int getSelectionEnd() {
9343 return Selection.getSelectionEnd(getText());
9344 }
9345
9346 /**
9347 * Return true iff there is a selection inside this text view.
9348 */
9349 public boolean hasSelection() {
9350 final int selectionStart = getSelectionStart();
9351 final int selectionEnd = getSelectionEnd();
9352
Justin Klaassenb8042fc2018-04-15 00:41:15 -04009353 return selectionStart >= 0 && selectionEnd > 0 && selectionStart != selectionEnd;
Justin Klaassen10d07c82017-09-15 17:58:39 -04009354 }
9355
9356 String getSelectedText() {
9357 if (!hasSelection()) {
9358 return null;
9359 }
9360
9361 final int start = getSelectionStart();
9362 final int end = getSelectionEnd();
9363 return String.valueOf(
9364 start > end ? mText.subSequence(end, start) : mText.subSequence(start, end));
9365 }
9366
9367 /**
9368 * Sets the properties of this field (lines, horizontally scrolling,
9369 * transformation method) to be for a single-line input.
9370 *
9371 * @attr ref android.R.styleable#TextView_singleLine
9372 */
9373 public void setSingleLine() {
9374 setSingleLine(true);
9375 }
9376
9377 /**
9378 * Sets the properties of this field to transform input to ALL CAPS
9379 * display. This may use a "small caps" formatting if available.
9380 * This setting will be ignored if this field is editable or selectable.
9381 *
9382 * This call replaces the current transformation method. Disabling this
9383 * will not necessarily restore the previous behavior from before this
9384 * was enabled.
9385 *
9386 * @see #setTransformationMethod(TransformationMethod)
9387 * @attr ref android.R.styleable#TextView_textAllCaps
9388 */
9389 public void setAllCaps(boolean allCaps) {
9390 if (allCaps) {
9391 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
9392 } else {
9393 setTransformationMethod(null);
9394 }
9395 }
9396
9397 /**
9398 *
Jeff Davidsona192cc22018-02-08 15:30:06 -08009399 * Checks whether the transformation method applied to this TextView is set to ALL CAPS.
Justin Klaassen10d07c82017-09-15 17:58:39 -04009400 * @return Whether the current transformation method is for ALL CAPS.
9401 *
9402 * @see #setAllCaps(boolean)
9403 * @see #setTransformationMethod(TransformationMethod)
9404 */
9405 public boolean isAllCaps() {
9406 final TransformationMethod method = getTransformationMethod();
9407 return method != null && method instanceof AllCapsTransformationMethod;
9408 }
9409
9410 /**
9411 * If true, sets the properties of this field (number of lines, horizontally scrolling,
9412 * transformation method) to be for a single-line input; if false, restores these to the default
9413 * conditions.
9414 *
9415 * Note that the default conditions are not necessarily those that were in effect prior this
9416 * method, and you may want to reset these properties to your custom values.
9417 *
9418 * @attr ref android.R.styleable#TextView_singleLine
9419 */
9420 @android.view.RemotableViewMethod
9421 public void setSingleLine(boolean singleLine) {
9422 // Could be used, but may break backward compatibility.
9423 // if (mSingleLine == singleLine) return;
9424 setInputTypeSingleLine(singleLine);
9425 applySingleLine(singleLine, true, true);
9426 }
9427
9428 /**
9429 * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
9430 * @param singleLine
9431 */
9432 private void setInputTypeSingleLine(boolean singleLine) {
9433 if (mEditor != null
9434 && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
9435 == EditorInfo.TYPE_CLASS_TEXT) {
9436 if (singleLine) {
9437 mEditor.mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
9438 } else {
9439 mEditor.mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
9440 }
9441 }
9442 }
9443
9444 private void applySingleLine(boolean singleLine, boolean applyTransformation,
9445 boolean changeMaxLines) {
9446 mSingleLine = singleLine;
9447 if (singleLine) {
9448 setLines(1);
9449 setHorizontallyScrolling(true);
9450 if (applyTransformation) {
9451 setTransformationMethod(SingleLineTransformationMethod.getInstance());
9452 }
9453 } else {
9454 if (changeMaxLines) {
9455 setMaxLines(Integer.MAX_VALUE);
9456 }
9457 setHorizontallyScrolling(false);
9458 if (applyTransformation) {
9459 setTransformationMethod(null);
9460 }
9461 }
9462 }
9463
9464 /**
9465 * Causes words in the text that are longer than the view's width
9466 * to be ellipsized instead of broken in the middle. You may also
9467 * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
9468 * to constrain the text to a single line. Use <code>null</code>
9469 * to turn off ellipsizing.
9470 *
9471 * If {@link #setMaxLines} has been used to set two or more lines,
9472 * only {@link android.text.TextUtils.TruncateAt#END} and
9473 * {@link android.text.TextUtils.TruncateAt#MARQUEE} are supported
9474 * (other ellipsizing types will not do anything).
9475 *
9476 * @attr ref android.R.styleable#TextView_ellipsize
9477 */
9478 public void setEllipsize(TextUtils.TruncateAt where) {
9479 // TruncateAt is an enum. != comparison is ok between these singleton objects.
9480 if (mEllipsize != where) {
9481 mEllipsize = where;
9482
9483 if (mLayout != null) {
9484 nullLayouts();
9485 requestLayout();
9486 invalidate();
9487 }
9488 }
9489 }
9490
9491 /**
9492 * Sets how many times to repeat the marquee animation. Only applied if the
9493 * TextView has marquee enabled. Set to -1 to repeat indefinitely.
9494 *
9495 * @see #getMarqueeRepeatLimit()
9496 *
9497 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
9498 */
9499 public void setMarqueeRepeatLimit(int marqueeLimit) {
9500 mMarqueeRepeatLimit = marqueeLimit;
9501 }
9502
9503 /**
9504 * Gets the number of times the marquee animation is repeated. Only meaningful if the
9505 * TextView has marquee enabled.
9506 *
9507 * @return the number of times the marquee animation is repeated. -1 if the animation
9508 * repeats indefinitely
9509 *
9510 * @see #setMarqueeRepeatLimit(int)
9511 *
9512 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
9513 */
9514 public int getMarqueeRepeatLimit() {
9515 return mMarqueeRepeatLimit;
9516 }
9517
9518 /**
9519 * Returns where, if anywhere, words that are longer than the view
9520 * is wide should be ellipsized.
9521 */
9522 @ViewDebug.ExportedProperty
9523 public TextUtils.TruncateAt getEllipsize() {
9524 return mEllipsize;
9525 }
9526
9527 /**
9528 * Set the TextView so that when it takes focus, all the text is
9529 * selected.
9530 *
9531 * @attr ref android.R.styleable#TextView_selectAllOnFocus
9532 */
9533 @android.view.RemotableViewMethod
9534 public void setSelectAllOnFocus(boolean selectAllOnFocus) {
9535 createEditorIfNeeded();
9536 mEditor.mSelectAllOnFocus = selectAllOnFocus;
9537
9538 if (selectAllOnFocus && !(mText instanceof Spannable)) {
9539 setText(mText, BufferType.SPANNABLE);
9540 }
9541 }
9542
9543 /**
9544 * Set whether the cursor is visible. The default is true. Note that this property only
9545 * makes sense for editable TextView.
9546 *
9547 * @see #isCursorVisible()
9548 *
9549 * @attr ref android.R.styleable#TextView_cursorVisible
9550 */
9551 @android.view.RemotableViewMethod
9552 public void setCursorVisible(boolean visible) {
9553 if (visible && mEditor == null) return; // visible is the default value with no edit data
9554 createEditorIfNeeded();
9555 if (mEditor.mCursorVisible != visible) {
9556 mEditor.mCursorVisible = visible;
9557 invalidate();
9558
9559 mEditor.makeBlink();
9560
9561 // InsertionPointCursorController depends on mCursorVisible
9562 mEditor.prepareCursorControllers();
9563 }
9564 }
9565
9566 /**
9567 * @return whether or not the cursor is visible (assuming this TextView is editable)
9568 *
9569 * @see #setCursorVisible(boolean)
9570 *
9571 * @attr ref android.R.styleable#TextView_cursorVisible
9572 */
9573 public boolean isCursorVisible() {
9574 // true is the default value
9575 return mEditor == null ? true : mEditor.mCursorVisible;
9576 }
9577
9578 private boolean canMarquee() {
9579 int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
9580 return width > 0 && (mLayout.getLineWidth(0) > width
9581 || (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null
9582 && mSavedMarqueeModeLayout.getLineWidth(0) > width));
9583 }
9584
9585 private void startMarquee() {
9586 // Do not ellipsize EditText
9587 if (getKeyListener() != null) return;
9588
9589 if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
9590 return;
9591 }
9592
9593 if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected())
9594 && getLineCount() == 1 && canMarquee()) {
9595
9596 if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
9597 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
9598 final Layout tmp = mLayout;
9599 mLayout = mSavedMarqueeModeLayout;
9600 mSavedMarqueeModeLayout = tmp;
9601 setHorizontalFadingEdgeEnabled(true);
9602 requestLayout();
9603 invalidate();
9604 }
9605
9606 if (mMarquee == null) mMarquee = new Marquee(this);
9607 mMarquee.start(mMarqueeRepeatLimit);
9608 }
9609 }
9610
9611 private void stopMarquee() {
9612 if (mMarquee != null && !mMarquee.isStopped()) {
9613 mMarquee.stop();
9614 }
9615
9616 if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) {
9617 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
9618 final Layout tmp = mSavedMarqueeModeLayout;
9619 mSavedMarqueeModeLayout = mLayout;
9620 mLayout = tmp;
9621 setHorizontalFadingEdgeEnabled(false);
9622 requestLayout();
9623 invalidate();
9624 }
9625 }
9626
9627 private void startStopMarquee(boolean start) {
9628 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
9629 if (start) {
9630 startMarquee();
9631 } else {
9632 stopMarquee();
9633 }
9634 }
9635 }
9636
9637 /**
9638 * This method is called when the text is changed, in case any subclasses
9639 * would like to know.
9640 *
9641 * Within <code>text</code>, the <code>lengthAfter</code> characters
9642 * beginning at <code>start</code> have just replaced old text that had
9643 * length <code>lengthBefore</code>. It is an error to attempt to make
9644 * changes to <code>text</code> from this callback.
9645 *
9646 * @param text The text the TextView is displaying
9647 * @param start The offset of the start of the range of the text that was
9648 * modified
9649 * @param lengthBefore The length of the former text that has been replaced
9650 * @param lengthAfter The length of the replacement modified text
9651 */
9652 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
9653 // intentionally empty, template pattern method can be overridden by subclasses
9654 }
9655
9656 /**
9657 * This method is called when the selection has changed, in case any
9658 * subclasses would like to know.
9659 *
9660 * @param selStart The new selection start location.
9661 * @param selEnd The new selection end location.
9662 */
9663 protected void onSelectionChanged(int selStart, int selEnd) {
9664 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
9665 }
9666
9667 /**
9668 * Adds a TextWatcher to the list of those whose methods are called
9669 * whenever this TextView's text changes.
9670 * <p>
9671 * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
9672 * not called after {@link #setText} calls. Now, doing {@link #setText}
9673 * if there are any text changed listeners forces the buffer type to
9674 * Editable if it would not otherwise be and does call this method.
9675 */
9676 public void addTextChangedListener(TextWatcher watcher) {
9677 if (mListeners == null) {
9678 mListeners = new ArrayList<TextWatcher>();
9679 }
9680
9681 mListeners.add(watcher);
9682 }
9683
9684 /**
9685 * Removes the specified TextWatcher from the list of those whose
9686 * methods are called
9687 * whenever this TextView's text changes.
9688 */
9689 public void removeTextChangedListener(TextWatcher watcher) {
9690 if (mListeners != null) {
9691 int i = mListeners.indexOf(watcher);
9692
9693 if (i >= 0) {
9694 mListeners.remove(i);
9695 }
9696 }
9697 }
9698
9699 private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
9700 if (mListeners != null) {
9701 final ArrayList<TextWatcher> list = mListeners;
9702 final int count = list.size();
9703 for (int i = 0; i < count; i++) {
9704 list.get(i).beforeTextChanged(text, start, before, after);
9705 }
9706 }
9707
9708 // The spans that are inside or intersect the modified region no longer make sense
9709 removeIntersectingNonAdjacentSpans(start, start + before, SpellCheckSpan.class);
9710 removeIntersectingNonAdjacentSpans(start, start + before, SuggestionSpan.class);
9711 }
9712
9713 // Removes all spans that are inside or actually overlap the start..end range
9714 private <T> void removeIntersectingNonAdjacentSpans(int start, int end, Class<T> type) {
9715 if (!(mText instanceof Editable)) return;
9716 Editable text = (Editable) mText;
9717
9718 T[] spans = text.getSpans(start, end, type);
9719 final int length = spans.length;
9720 for (int i = 0; i < length; i++) {
9721 final int spanStart = text.getSpanStart(spans[i]);
9722 final int spanEnd = text.getSpanEnd(spans[i]);
9723 if (spanEnd == start || spanStart == end) break;
9724 text.removeSpan(spans[i]);
9725 }
9726 }
9727
9728 void removeAdjacentSuggestionSpans(final int pos) {
9729 if (!(mText instanceof Editable)) return;
9730 final Editable text = (Editable) mText;
9731
9732 final SuggestionSpan[] spans = text.getSpans(pos, pos, SuggestionSpan.class);
9733 final int length = spans.length;
9734 for (int i = 0; i < length; i++) {
9735 final int spanStart = text.getSpanStart(spans[i]);
9736 final int spanEnd = text.getSpanEnd(spans[i]);
9737 if (spanEnd == pos || spanStart == pos) {
9738 if (SpellChecker.haveWordBoundariesChanged(text, pos, pos, spanStart, spanEnd)) {
9739 text.removeSpan(spans[i]);
9740 }
9741 }
9742 }
9743 }
9744
9745 /**
9746 * Not private so it can be called from an inner class without going
9747 * through a thunk.
9748 */
9749 void sendOnTextChanged(CharSequence text, int start, int before, int after) {
9750 if (mListeners != null) {
9751 final ArrayList<TextWatcher> list = mListeners;
9752 final int count = list.size();
9753 for (int i = 0; i < count; i++) {
9754 list.get(i).onTextChanged(text, start, before, after);
9755 }
9756 }
9757
9758 if (mEditor != null) mEditor.sendOnTextChanged(start, before, after);
9759 }
9760
9761 /**
9762 * Not private so it can be called from an inner class without going
9763 * through a thunk.
9764 */
9765 void sendAfterTextChanged(Editable text) {
9766 if (mListeners != null) {
9767 final ArrayList<TextWatcher> list = mListeners;
9768 final int count = list.size();
9769 for (int i = 0; i < count; i++) {
9770 list.get(i).afterTextChanged(text);
9771 }
9772 }
9773
9774 // Always notify AutoFillManager - it will return right away if autofill is disabled.
9775 notifyAutoFillManagerAfterTextChangedIfNeeded();
9776
9777 hideErrorIfUnchanged();
9778 }
9779
9780 private void notifyAutoFillManagerAfterTextChangedIfNeeded() {
9781 // It is important to not check whether the view is important for autofill
9782 // since the user can trigger autofill manually on not important views.
9783 if (!isAutofillable()) {
9784 return;
9785 }
9786 final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
Justin Klaassen4d01eea2018-04-03 23:21:57 -04009787 if (afm == null) {
9788 return;
9789 }
9790
9791 if (mLastValueSentToAutofillManager == null
9792 || !mLastValueSentToAutofillManager.equals(mText)) {
Jeff Davidsona192cc22018-02-08 15:30:06 -08009793 if (android.view.autofill.Helper.sVerbose) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04009794 Log.v(LOG_TAG, "notifying AFM after text changed");
Justin Klaassen10d07c82017-09-15 17:58:39 -04009795 }
9796 afm.notifyValueChanged(TextView.this);
Justin Klaassen4d01eea2018-04-03 23:21:57 -04009797 mLastValueSentToAutofillManager = mText;
9798 } else {
9799 if (android.view.autofill.Helper.sVerbose) {
9800 Log.v(LOG_TAG, "not notifying AFM on unchanged text");
9801 }
Justin Klaassen10d07c82017-09-15 17:58:39 -04009802 }
9803 }
9804
9805 private boolean isAutofillable() {
9806 // It is important to not check whether the view is important for autofill
9807 // since the user can trigger autofill manually on not important views.
9808 return getAutofillType() != AUTOFILL_TYPE_NONE;
9809 }
9810
9811 void updateAfterEdit() {
9812 invalidate();
9813 int curs = getSelectionStart();
9814
9815 if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
9816 registerForPreDraw();
9817 }
9818
9819 checkForResize();
9820
9821 if (curs >= 0) {
9822 mHighlightPathBogus = true;
9823 if (mEditor != null) mEditor.makeBlink();
9824 bringPointIntoView(curs);
9825 }
9826 }
9827
9828 /**
9829 * Not private so it can be called from an inner class without going
9830 * through a thunk.
9831 */
9832 void handleTextChanged(CharSequence buffer, int start, int before, int after) {
9833 sLastCutCopyOrTextChangedTime = 0;
9834
9835 final Editor.InputMethodState ims = mEditor == null ? null : mEditor.mInputMethodState;
9836 if (ims == null || ims.mBatchEditNesting == 0) {
9837 updateAfterEdit();
9838 }
9839 if (ims != null) {
9840 ims.mContentChanged = true;
9841 if (ims.mChangedStart < 0) {
9842 ims.mChangedStart = start;
9843 ims.mChangedEnd = start + before;
9844 } else {
9845 ims.mChangedStart = Math.min(ims.mChangedStart, start);
9846 ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
9847 }
9848 ims.mChangedDelta += after - before;
9849 }
9850 resetErrorChangedFlag();
9851 sendOnTextChanged(buffer, start, before, after);
9852 onTextChanged(buffer, start, before, after);
9853 }
9854
9855 /**
9856 * Not private so it can be called from an inner class without going
9857 * through a thunk.
9858 */
9859 void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
9860 // XXX Make the start and end move together if this ends up
9861 // spending too much time invalidating.
9862
9863 boolean selChanged = false;
9864 int newSelStart = -1, newSelEnd = -1;
9865
9866 final Editor.InputMethodState ims = mEditor == null ? null : mEditor.mInputMethodState;
9867
9868 if (what == Selection.SELECTION_END) {
9869 selChanged = true;
9870 newSelEnd = newStart;
9871
9872 if (oldStart >= 0 || newStart >= 0) {
9873 invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
9874 checkForResize();
9875 registerForPreDraw();
9876 if (mEditor != null) mEditor.makeBlink();
9877 }
9878 }
9879
9880 if (what == Selection.SELECTION_START) {
9881 selChanged = true;
9882 newSelStart = newStart;
9883
9884 if (oldStart >= 0 || newStart >= 0) {
9885 int end = Selection.getSelectionEnd(buf);
9886 invalidateCursor(end, oldStart, newStart);
9887 }
9888 }
9889
9890 if (selChanged) {
9891 mHighlightPathBogus = true;
9892 if (mEditor != null && !isFocused()) mEditor.mSelectionMoved = true;
9893
9894 if ((buf.getSpanFlags(what) & Spanned.SPAN_INTERMEDIATE) == 0) {
9895 if (newSelStart < 0) {
9896 newSelStart = Selection.getSelectionStart(buf);
9897 }
9898 if (newSelEnd < 0) {
9899 newSelEnd = Selection.getSelectionEnd(buf);
9900 }
9901
9902 if (mEditor != null) {
9903 mEditor.refreshTextActionMode();
9904 if (!hasSelection()
9905 && mEditor.getTextActionMode() == null && hasTransientState()) {
9906 // User generated selection has been removed.
9907 setHasTransientState(false);
9908 }
9909 }
9910 onSelectionChanged(newSelStart, newSelEnd);
9911 }
9912 }
9913
9914 if (what instanceof UpdateAppearance || what instanceof ParagraphStyle
9915 || what instanceof CharacterStyle) {
9916 if (ims == null || ims.mBatchEditNesting == 0) {
9917 invalidate();
9918 mHighlightPathBogus = true;
9919 checkForResize();
9920 } else {
9921 ims.mContentChanged = true;
9922 }
9923 if (mEditor != null) {
9924 if (oldStart >= 0) mEditor.invalidateTextDisplayList(mLayout, oldStart, oldEnd);
9925 if (newStart >= 0) mEditor.invalidateTextDisplayList(mLayout, newStart, newEnd);
9926 mEditor.invalidateHandlesAndActionMode();
9927 }
9928 }
9929
9930 if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
9931 mHighlightPathBogus = true;
9932 if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
9933 ims.mSelectionModeChanged = true;
9934 }
9935
9936 if (Selection.getSelectionStart(buf) >= 0) {
9937 if (ims == null || ims.mBatchEditNesting == 0) {
9938 invalidateCursor();
9939 } else {
9940 ims.mCursorChanged = true;
9941 }
9942 }
9943 }
9944
9945 if (what instanceof ParcelableSpan) {
9946 // If this is a span that can be sent to a remote process,
9947 // the current extract editor would be interested in it.
9948 if (ims != null && ims.mExtractedTextRequest != null) {
9949 if (ims.mBatchEditNesting != 0) {
9950 if (oldStart >= 0) {
9951 if (ims.mChangedStart > oldStart) {
9952 ims.mChangedStart = oldStart;
9953 }
9954 if (ims.mChangedStart > oldEnd) {
9955 ims.mChangedStart = oldEnd;
9956 }
9957 }
9958 if (newStart >= 0) {
9959 if (ims.mChangedStart > newStart) {
9960 ims.mChangedStart = newStart;
9961 }
9962 if (ims.mChangedStart > newEnd) {
9963 ims.mChangedStart = newEnd;
9964 }
9965 }
9966 } else {
9967 if (DEBUG_EXTRACT) {
9968 Log.v(LOG_TAG, "Span change outside of batch: "
9969 + oldStart + "-" + oldEnd + ","
9970 + newStart + "-" + newEnd + " " + what);
9971 }
9972 ims.mContentChanged = true;
9973 }
9974 }
9975 }
9976
9977 if (mEditor != null && mEditor.mSpellChecker != null && newStart < 0
9978 && what instanceof SpellCheckSpan) {
9979 mEditor.mSpellChecker.onSpellCheckSpanRemoved((SpellCheckSpan) what);
9980 }
9981 }
9982
9983 @Override
9984 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
9985 if (isTemporarilyDetached()) {
9986 // If we are temporarily in the detach state, then do nothing.
9987 super.onFocusChanged(focused, direction, previouslyFocusedRect);
9988 return;
9989 }
9990
9991 if (mEditor != null) mEditor.onFocusChanged(focused, direction);
9992
9993 if (focused) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -04009994 if (mSpannable != null) {
9995 MetaKeyKeyListener.resetMetaState(mSpannable);
Justin Klaassen10d07c82017-09-15 17:58:39 -04009996 }
9997 }
9998
9999 startStopMarquee(focused);
10000
10001 if (mTransformation != null) {
10002 mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
10003 }
10004
10005 super.onFocusChanged(focused, direction, previouslyFocusedRect);
10006 }
10007
10008 @Override
10009 public void onWindowFocusChanged(boolean hasWindowFocus) {
10010 super.onWindowFocusChanged(hasWindowFocus);
10011
10012 if (mEditor != null) mEditor.onWindowFocusChanged(hasWindowFocus);
10013
10014 startStopMarquee(hasWindowFocus);
10015 }
10016
10017 @Override
10018 protected void onVisibilityChanged(View changedView, int visibility) {
10019 super.onVisibilityChanged(changedView, visibility);
10020 if (mEditor != null && visibility != VISIBLE) {
10021 mEditor.hideCursorAndSpanControllers();
10022 stopTextActionMode();
10023 }
10024 }
10025
10026 /**
10027 * Use {@link BaseInputConnection#removeComposingSpans
10028 * BaseInputConnection.removeComposingSpans()} to remove any IME composing
10029 * state from this text view.
10030 */
10031 public void clearComposingText() {
10032 if (mText instanceof Spannable) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -040010033 BaseInputConnection.removeComposingSpans(mSpannable);
Justin Klaassen10d07c82017-09-15 17:58:39 -040010034 }
10035 }
10036
10037 @Override
10038 public void setSelected(boolean selected) {
10039 boolean wasSelected = isSelected();
10040
10041 super.setSelected(selected);
10042
10043 if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
10044 if (selected) {
10045 startMarquee();
10046 } else {
10047 stopMarquee();
10048 }
10049 }
10050 }
10051
10052 @Override
10053 public boolean onTouchEvent(MotionEvent event) {
10054 final int action = event.getActionMasked();
10055 if (mEditor != null) {
10056 mEditor.onTouchEvent(event);
10057
10058 if (mEditor.mSelectionModifierCursorController != null
10059 && mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
10060 return true;
10061 }
10062 }
10063
10064 final boolean superResult = super.onTouchEvent(event);
10065
10066 /*
10067 * Don't handle the release after a long press, because it will move the selection away from
10068 * whatever the menu action was trying to affect. If the long press should have triggered an
10069 * insertion action mode, we can now actually show it.
10070 */
10071 if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
10072 mEditor.mDiscardNextActionUp = false;
10073
10074 if (mEditor.mIsInsertionActionModeStartPending) {
10075 mEditor.startInsertionActionMode();
10076 mEditor.mIsInsertionActionModeStartPending = false;
10077 }
10078 return superResult;
10079 }
10080
10081 final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
10082 && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
10083
10084 if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
10085 && mText instanceof Spannable && mLayout != null) {
10086 boolean handled = false;
10087
10088 if (mMovement != null) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -040010089 handled |= mMovement.onTouchEvent(this, mSpannable, event);
Justin Klaassen10d07c82017-09-15 17:58:39 -040010090 }
10091
10092 final boolean textIsSelectable = isTextSelectable();
10093 if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
10094 // The LinkMovementMethod which should handle taps on links has not been installed
10095 // on non editable text that support text selection.
10096 // We reproduce its behavior here to open links for these.
Justin Klaassen4d01eea2018-04-03 23:21:57 -040010097 ClickableSpan[] links = mSpannable.getSpans(getSelectionStart(),
Justin Klaassen10d07c82017-09-15 17:58:39 -040010098 getSelectionEnd(), ClickableSpan.class);
10099
10100 if (links.length > 0) {
10101 links[0].onClick(this);
10102 handled = true;
10103 }
10104 }
10105
10106 if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
10107 // Show the IME, except when selecting in read-only text.
10108 final InputMethodManager imm = InputMethodManager.peekInstance();
10109 viewClicked(imm);
10110 if (isTextEditable() && mEditor.mShowSoftInputOnFocus && imm != null) {
10111 imm.showSoftInput(this, 0);
10112 }
10113
10114 // The above condition ensures that the mEditor is not null
10115 mEditor.onTouchUpEvent(event);
10116
10117 handled = true;
10118 }
10119
10120 if (handled) {
10121 return true;
10122 }
10123 }
10124
10125 return superResult;
10126 }
10127
10128 @Override
10129 public boolean onGenericMotionEvent(MotionEvent event) {
10130 if (mMovement != null && mText instanceof Spannable && mLayout != null) {
10131 try {
Justin Klaassen4d01eea2018-04-03 23:21:57 -040010132 if (mMovement.onGenericMotionEvent(this, mSpannable, event)) {
Justin Klaassen10d07c82017-09-15 17:58:39 -040010133 return true;
10134 }
10135 } catch (AbstractMethodError ex) {
10136 // onGenericMotionEvent was added to the MovementMethod interface in API 12.
10137 // Ignore its absence in case third party applications implemented the
10138 // interface directly.
10139 }
10140 }
10141 return super.onGenericMotionEvent(event);
10142 }
10143
10144 @Override
10145 protected void onCreateContextMenu(ContextMenu menu) {
10146 if (mEditor != null) {
10147 mEditor.onCreateContextMenu(menu);
10148 }
10149 }
10150
10151 @Override
10152 public boolean showContextMenu() {
10153 if (mEditor != null) {
10154 mEditor.setContextMenuAnchor(Float.NaN, Float.NaN);
10155 }
10156 return super.showContextMenu();
10157 }
10158
10159 @Override
10160 public boolean showContextMenu(float x, float y) {
10161 if (mEditor != null) {
10162 mEditor.setContextMenuAnchor(x, y);
10163 }
10164 return super.showContextMenu(x, y);
10165 }
10166
10167 /**
10168 * @return True iff this TextView contains a text that can be edited, or if this is
10169 * a selectable TextView.
10170 */
10171 boolean isTextEditable() {
10172 return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
10173 }
10174
10175 /**
10176 * Returns true, only while processing a touch gesture, if the initial
10177 * touch down event caused focus to move to the text view and as a result
10178 * its selection changed. Only valid while processing the touch gesture
10179 * of interest, in an editable text view.
10180 */
10181 public boolean didTouchFocusSelect() {
10182 return mEditor != null && mEditor.mTouchFocusSelected;
10183 }
10184
10185 @Override
10186 public void cancelLongPress() {
10187 super.cancelLongPress();
10188 if (mEditor != null) mEditor.mIgnoreActionUpEvent = true;
10189 }
10190
10191 @Override
10192 public boolean onTrackballEvent(MotionEvent event) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -040010193 if (mMovement != null && mSpannable != null && mLayout != null) {
10194 if (mMovement.onTrackballEvent(this, mSpannable, event)) {
Justin Klaassen10d07c82017-09-15 17:58:39 -040010195 return true;
10196 }
10197 }
10198
10199 return super.onTrackballEvent(event);
10200 }
10201
10202 /**
10203 * Sets the Scroller used for producing a scrolling animation
10204 *
10205 * @param s A Scroller instance
10206 */
10207 public void setScroller(Scroller s) {
10208 mScroller = s;
10209 }
10210
10211 @Override
10212 protected float getLeftFadingEdgeStrength() {
10213 if (isMarqueeFadeEnabled() && mMarquee != null && !mMarquee.isStopped()) {
10214 final Marquee marquee = mMarquee;
10215 if (marquee.shouldDrawLeftFade()) {
10216 return getHorizontalFadingEdgeStrength(marquee.getScroll(), 0.0f);
10217 } else {
10218 return 0.0f;
10219 }
10220 } else if (getLineCount() == 1) {
10221 final float lineLeft = getLayout().getLineLeft(0);
10222 if (lineLeft > mScrollX) return 0.0f;
10223 return getHorizontalFadingEdgeStrength(mScrollX, lineLeft);
10224 }
10225 return super.getLeftFadingEdgeStrength();
10226 }
10227
10228 @Override
10229 protected float getRightFadingEdgeStrength() {
10230 if (isMarqueeFadeEnabled() && mMarquee != null && !mMarquee.isStopped()) {
10231 final Marquee marquee = mMarquee;
10232 return getHorizontalFadingEdgeStrength(marquee.getMaxFadeScroll(), marquee.getScroll());
10233 } else if (getLineCount() == 1) {
10234 final float rightEdge = mScrollX +
10235 (getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight());
10236 final float lineRight = getLayout().getLineRight(0);
10237 if (lineRight < rightEdge) return 0.0f;
10238 return getHorizontalFadingEdgeStrength(rightEdge, lineRight);
10239 }
10240 return super.getRightFadingEdgeStrength();
10241 }
10242
10243 /**
10244 * Calculates the fading edge strength as the ratio of the distance between two
10245 * horizontal positions to {@link View#getHorizontalFadingEdgeLength()}. Uses the absolute
10246 * value for the distance calculation.
10247 *
10248 * @param position1 A horizontal position.
10249 * @param position2 A horizontal position.
10250 * @return Fading edge strength between [0.0f, 1.0f].
10251 */
10252 @FloatRange(from = 0.0, to = 1.0)
10253 private float getHorizontalFadingEdgeStrength(float position1, float position2) {
10254 final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength();
10255 if (horizontalFadingEdgeLength == 0) return 0.0f;
10256 final float diff = Math.abs(position1 - position2);
10257 if (diff > horizontalFadingEdgeLength) return 1.0f;
10258 return diff / horizontalFadingEdgeLength;
10259 }
10260
10261 private boolean isMarqueeFadeEnabled() {
10262 return mEllipsize == TextUtils.TruncateAt.MARQUEE
10263 && mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
10264 }
10265
10266 @Override
10267 protected int computeHorizontalScrollRange() {
10268 if (mLayout != null) {
10269 return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT
10270 ? (int) mLayout.getLineWidth(0) : mLayout.getWidth();
10271 }
10272
10273 return super.computeHorizontalScrollRange();
10274 }
10275
10276 @Override
10277 protected int computeVerticalScrollRange() {
10278 if (mLayout != null) {
10279 return mLayout.getHeight();
10280 }
10281 return super.computeVerticalScrollRange();
10282 }
10283
10284 @Override
10285 protected int computeVerticalScrollExtent() {
10286 return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
10287 }
10288
10289 @Override
10290 public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
10291 super.findViewsWithText(outViews, searched, flags);
10292 if (!outViews.contains(this) && (flags & FIND_VIEWS_WITH_TEXT) != 0
10293 && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mText)) {
10294 String searchedLowerCase = searched.toString().toLowerCase();
10295 String textLowerCase = mText.toString().toLowerCase();
10296 if (textLowerCase.contains(searchedLowerCase)) {
10297 outViews.add(this);
10298 }
10299 }
10300 }
10301
10302 /**
10303 * Type of the text buffer that defines the characteristics of the text such as static,
10304 * styleable, or editable.
10305 */
10306 public enum BufferType {
10307 NORMAL, SPANNABLE, EDITABLE
10308 }
10309
10310 /**
10311 * Returns the TextView_textColor attribute from the TypedArray, if set, or
10312 * the TextAppearance_textColor from the TextView_textAppearance attribute,
10313 * if TextView_textColor was not set directly.
10314 *
10315 * @removed
10316 */
10317 public static ColorStateList getTextColors(Context context, TypedArray attrs) {
10318 if (attrs == null) {
10319 // Preserve behavior prior to removal of this API.
10320 throw new NullPointerException();
10321 }
10322
10323 // It's not safe to use this method from apps. The parameter 'attrs'
10324 // must have been obtained using the TextView filter array which is not
10325 // available to the SDK. As such, we grab a default TypedArray with the
10326 // right filter instead here.
10327 final TypedArray a = context.obtainStyledAttributes(R.styleable.TextView);
10328 ColorStateList colors = a.getColorStateList(R.styleable.TextView_textColor);
10329 if (colors == null) {
10330 final int ap = a.getResourceId(R.styleable.TextView_textAppearance, 0);
10331 if (ap != 0) {
10332 final TypedArray appearance = context.obtainStyledAttributes(
10333 ap, R.styleable.TextAppearance);
10334 colors = appearance.getColorStateList(R.styleable.TextAppearance_textColor);
10335 appearance.recycle();
10336 }
10337 }
10338 a.recycle();
10339
10340 return colors;
10341 }
10342
10343 /**
10344 * Returns the default color from the TextView_textColor attribute from the
10345 * AttributeSet, if set, or the default color from the
10346 * TextAppearance_textColor from the TextView_textAppearance attribute, if
10347 * TextView_textColor was not set directly.
10348 *
10349 * @removed
10350 */
10351 public static int getTextColor(Context context, TypedArray attrs, int def) {
10352 final ColorStateList colors = getTextColors(context, attrs);
10353 if (colors == null) {
10354 return def;
10355 } else {
10356 return colors.getDefaultColor();
10357 }
10358 }
10359
10360 @Override
10361 public boolean onKeyShortcut(int keyCode, KeyEvent event) {
10362 if (event.hasModifiers(KeyEvent.META_CTRL_ON)) {
10363 // Handle Ctrl-only shortcuts.
10364 switch (keyCode) {
10365 case KeyEvent.KEYCODE_A:
10366 if (canSelectText()) {
10367 return onTextContextMenuItem(ID_SELECT_ALL);
10368 }
10369 break;
10370 case KeyEvent.KEYCODE_Z:
10371 if (canUndo()) {
10372 return onTextContextMenuItem(ID_UNDO);
10373 }
10374 break;
10375 case KeyEvent.KEYCODE_X:
10376 if (canCut()) {
10377 return onTextContextMenuItem(ID_CUT);
10378 }
10379 break;
10380 case KeyEvent.KEYCODE_C:
10381 if (canCopy()) {
10382 return onTextContextMenuItem(ID_COPY);
10383 }
10384 break;
10385 case KeyEvent.KEYCODE_V:
10386 if (canPaste()) {
10387 return onTextContextMenuItem(ID_PASTE);
10388 }
10389 break;
10390 }
10391 } else if (event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) {
10392 // Handle Ctrl-Shift shortcuts.
10393 switch (keyCode) {
10394 case KeyEvent.KEYCODE_Z:
10395 if (canRedo()) {
10396 return onTextContextMenuItem(ID_REDO);
10397 }
10398 break;
10399 case KeyEvent.KEYCODE_V:
10400 if (canPaste()) {
10401 return onTextContextMenuItem(ID_PASTE_AS_PLAIN_TEXT);
10402 }
10403 }
10404 }
10405 return super.onKeyShortcut(keyCode, event);
10406 }
10407
10408 /**
10409 * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
10410 * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
10411 * a selection controller (see {@link Editor#prepareCursorControllers()}), but this is not
10412 * sufficient.
10413 */
10414 boolean canSelectText() {
10415 return mText.length() != 0 && mEditor != null && mEditor.hasSelectionController();
10416 }
10417
10418 /**
10419 * Test based on the <i>intrinsic</i> charateristics of the TextView.
10420 * The text must be spannable and the movement method must allow for arbitary selection.
10421 *
10422 * See also {@link #canSelectText()}.
10423 */
10424 boolean textCanBeSelected() {
10425 // prepareCursorController() relies on this method.
10426 // If you change this condition, make sure prepareCursorController is called anywhere
10427 // the value of this condition might be changed.
10428 if (mMovement == null || !mMovement.canSelectArbitrarily()) return false;
10429 return isTextEditable()
10430 || (isTextSelectable() && mText instanceof Spannable && isEnabled());
10431 }
10432
10433 private Locale getTextServicesLocale(boolean allowNullLocale) {
10434 // Start fetching the text services locale asynchronously.
10435 updateTextServicesLocaleAsync();
10436 // If !allowNullLocale and there is no cached text services locale, just return the default
10437 // locale.
10438 return (mCurrentSpellCheckerLocaleCache == null && !allowNullLocale) ? Locale.getDefault()
10439 : mCurrentSpellCheckerLocaleCache;
10440 }
10441
10442 /**
10443 * This is a temporary method. Future versions may support multi-locale text.
10444 * Caveat: This method may not return the latest text services locale, but this should be
10445 * acceptable and it's more important to make this method asynchronous.
10446 *
10447 * @return The locale that should be used for a word iterator
10448 * in this TextView, based on the current spell checker settings,
10449 * the current IME's locale, or the system default locale.
10450 * Please note that a word iterator in this TextView is different from another word iterator
10451 * used by SpellChecker.java of TextView. This method should be used for the former.
10452 * @hide
10453 */
10454 // TODO: Support multi-locale
10455 // TODO: Update the text services locale immediately after the keyboard locale is switched
10456 // by catching intent of keyboard switch event
10457 public Locale getTextServicesLocale() {
10458 return getTextServicesLocale(false /* allowNullLocale */);
10459 }
10460
10461 /**
10462 * @return {@code true} if this TextView is specialized for showing and interacting with the
10463 * extracted text in a full-screen input method.
10464 * @hide
10465 */
10466 public boolean isInExtractedMode() {
10467 return false;
10468 }
10469
10470 /**
10471 * @return {@code true} if this widget supports auto-sizing text and has been configured to
10472 * auto-size.
10473 */
10474 private boolean isAutoSizeEnabled() {
10475 return supportsAutoSizeText() && mAutoSizeTextType != AUTO_SIZE_TEXT_TYPE_NONE;
10476 }
10477
10478 /**
10479 * @return {@code true} if this TextView supports auto-sizing text to fit within its container.
10480 * @hide
10481 */
10482 protected boolean supportsAutoSizeText() {
10483 return true;
10484 }
10485
10486 /**
10487 * This is a temporary method. Future versions may support multi-locale text.
10488 * Caveat: This method may not return the latest spell checker locale, but this should be
10489 * acceptable and it's more important to make this method asynchronous.
10490 *
10491 * @return The locale that should be used for a spell checker in this TextView,
10492 * based on the current spell checker settings, the current IME's locale, or the system default
10493 * locale.
10494 * @hide
10495 */
10496 public Locale getSpellCheckerLocale() {
10497 return getTextServicesLocale(true /* allowNullLocale */);
10498 }
10499
10500 private void updateTextServicesLocaleAsync() {
10501 // AsyncTask.execute() uses a serial executor which means we don't have
10502 // to lock around updateTextServicesLocaleLocked() to prevent it from
10503 // being executed n times in parallel.
10504 AsyncTask.execute(new Runnable() {
10505 @Override
10506 public void run() {
10507 updateTextServicesLocaleLocked();
10508 }
10509 });
10510 }
10511
10512 private void updateTextServicesLocaleLocked() {
10513 final TextServicesManager textServicesManager = (TextServicesManager)
10514 mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
10515 final SpellCheckerSubtype subtype = textServicesManager.getCurrentSpellCheckerSubtype(true);
10516 final Locale locale;
10517 if (subtype != null) {
10518 locale = subtype.getLocaleObject();
10519 } else {
10520 locale = null;
10521 }
10522 mCurrentSpellCheckerLocaleCache = locale;
10523 }
10524
10525 void onLocaleChanged() {
10526 mEditor.onLocaleChanged();
10527 }
10528
10529 /**
10530 * This method is used by the ArrowKeyMovementMethod to jump from one word to the other.
10531 * Made available to achieve a consistent behavior.
10532 * @hide
10533 */
10534 public WordIterator getWordIterator() {
10535 if (mEditor != null) {
10536 return mEditor.getWordIterator();
10537 } else {
10538 return null;
10539 }
10540 }
10541
10542 /** @hide */
10543 @Override
10544 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
10545 super.onPopulateAccessibilityEventInternal(event);
10546
10547 final CharSequence text = getTextForAccessibility();
10548 if (!TextUtils.isEmpty(text)) {
10549 event.getText().add(text);
10550 }
10551 }
10552
10553 @Override
10554 public CharSequence getAccessibilityClassName() {
10555 return TextView.class.getName();
10556 }
10557
10558 @Override
10559 public void onProvideStructure(ViewStructure structure) {
10560 super.onProvideStructure(structure);
10561 onProvideAutoStructureForAssistOrAutofill(structure, false);
10562 }
10563
10564 @Override
10565 public void onProvideAutofillStructure(ViewStructure structure, int flags) {
10566 super.onProvideAutofillStructure(structure, flags);
10567 onProvideAutoStructureForAssistOrAutofill(structure, true);
10568 }
10569
10570 private void onProvideAutoStructureForAssistOrAutofill(ViewStructure structure,
10571 boolean forAutofill) {
10572 final boolean isPassword = hasPasswordTransformationMethod()
10573 || isPasswordInputType(getInputType());
10574 if (forAutofill) {
Jeff Davidsona192cc22018-02-08 15:30:06 -080010575 structure.setDataIsSensitive(!mTextSetFromXmlOrResourceId);
10576 if (mTextId != ResourceId.ID_NULL) {
10577 try {
10578 structure.setTextIdEntry(getResources().getResourceEntryName(mTextId));
10579 } catch (Resources.NotFoundException e) {
10580 if (android.view.autofill.Helper.sVerbose) {
10581 Log.v(LOG_TAG, "onProvideAutofillStructure(): cannot set name for text id "
10582 + mTextId + ": " + e.getMessage());
10583 }
10584 }
10585 }
Justin Klaassen10d07c82017-09-15 17:58:39 -040010586 }
10587
10588 if (!isPassword || forAutofill) {
10589 if (mLayout == null) {
10590 assumeLayout();
10591 }
10592 Layout layout = mLayout;
10593 final int lineCount = layout.getLineCount();
10594 if (lineCount <= 1) {
10595 // Simple case: this is a single line.
10596 final CharSequence text = getText();
10597 if (forAutofill) {
10598 structure.setText(text);
10599 } else {
10600 structure.setText(text, getSelectionStart(), getSelectionEnd());
10601 }
10602 } else {
10603 // Complex case: multi-line, could be scrolled or within a scroll container
10604 // so some lines are not visible.
10605 final int[] tmpCords = new int[2];
10606 getLocationInWindow(tmpCords);
10607 final int topWindowLocation = tmpCords[1];
10608 View root = this;
10609 ViewParent viewParent = getParent();
10610 while (viewParent instanceof View) {
10611 root = (View) viewParent;
10612 viewParent = root.getParent();
10613 }
10614 final int windowHeight = root.getHeight();
10615 final int topLine;
10616 final int bottomLine;
10617 if (topWindowLocation >= 0) {
10618 // The top of the view is fully within its window; start text at line 0.
10619 topLine = getLineAtCoordinateUnclamped(0);
10620 bottomLine = getLineAtCoordinateUnclamped(windowHeight - 1);
10621 } else {
10622 // The top of hte window has scrolled off the top of the window; figure out
10623 // the starting line for this.
10624 topLine = getLineAtCoordinateUnclamped(-topWindowLocation);
10625 bottomLine = getLineAtCoordinateUnclamped(windowHeight - 1 - topWindowLocation);
10626 }
10627 // We want to return some contextual lines above/below the lines that are
10628 // actually visible.
10629 int expandedTopLine = topLine - (bottomLine - topLine) / 2;
10630 if (expandedTopLine < 0) {
10631 expandedTopLine = 0;
10632 }
10633 int expandedBottomLine = bottomLine + (bottomLine - topLine) / 2;
10634 if (expandedBottomLine >= lineCount) {
10635 expandedBottomLine = lineCount - 1;
10636 }
10637
10638 // Convert lines into character offsets.
10639 int expandedTopChar = layout.getLineStart(expandedTopLine);
10640 int expandedBottomChar = layout.getLineEnd(expandedBottomLine);
10641
10642 // Take into account selection -- if there is a selection, we need to expand
10643 // the text we are returning to include that selection.
10644 final int selStart = getSelectionStart();
10645 final int selEnd = getSelectionEnd();
10646 if (selStart < selEnd) {
10647 if (selStart < expandedTopChar) {
10648 expandedTopChar = selStart;
10649 }
10650 if (selEnd > expandedBottomChar) {
10651 expandedBottomChar = selEnd;
10652 }
10653 }
10654
10655 // Get the text and trim it to the range we are reporting.
10656 CharSequence text = getText();
10657 if (expandedTopChar > 0 || expandedBottomChar < text.length()) {
10658 text = text.subSequence(expandedTopChar, expandedBottomChar);
10659 }
10660
10661 if (forAutofill) {
10662 structure.setText(text);
10663 } else {
10664 structure.setText(text, selStart - expandedTopChar, selEnd - expandedTopChar);
10665
10666 final int[] lineOffsets = new int[bottomLine - topLine + 1];
10667 final int[] lineBaselines = new int[bottomLine - topLine + 1];
10668 final int baselineOffset = getBaselineOffset();
10669 for (int i = topLine; i <= bottomLine; i++) {
10670 lineOffsets[i - topLine] = layout.getLineStart(i);
10671 lineBaselines[i - topLine] = layout.getLineBaseline(i) + baselineOffset;
10672 }
10673 structure.setTextLines(lineOffsets, lineBaselines);
10674 }
10675 }
10676
10677 if (!forAutofill) {
10678 // Extract style information that applies to the TextView as a whole.
10679 int style = 0;
10680 int typefaceStyle = getTypefaceStyle();
10681 if ((typefaceStyle & Typeface.BOLD) != 0) {
10682 style |= AssistStructure.ViewNode.TEXT_STYLE_BOLD;
10683 }
10684 if ((typefaceStyle & Typeface.ITALIC) != 0) {
10685 style |= AssistStructure.ViewNode.TEXT_STYLE_ITALIC;
10686 }
10687
10688 // Global styles can also be set via TextView.setPaintFlags().
10689 int paintFlags = mTextPaint.getFlags();
10690 if ((paintFlags & Paint.FAKE_BOLD_TEXT_FLAG) != 0) {
10691 style |= AssistStructure.ViewNode.TEXT_STYLE_BOLD;
10692 }
10693 if ((paintFlags & Paint.UNDERLINE_TEXT_FLAG) != 0) {
10694 style |= AssistStructure.ViewNode.TEXT_STYLE_UNDERLINE;
10695 }
10696 if ((paintFlags & Paint.STRIKE_THRU_TEXT_FLAG) != 0) {
10697 style |= AssistStructure.ViewNode.TEXT_STYLE_STRIKE_THRU;
10698 }
10699
10700 // TextView does not have its own text background color. A background is either part
10701 // of the View (and can be any drawable) or a BackgroundColorSpan inside the text.
10702 structure.setTextStyle(getTextSize(), getCurrentTextColor(),
10703 AssistStructure.ViewNode.TEXT_COLOR_UNDEFINED /* bgColor */, style);
Justin Klaassen47ed54e2017-10-24 19:50:40 -040010704 } else {
10705 structure.setMinTextEms(getMinEms());
10706 structure.setMaxTextEms(getMaxEms());
10707 int maxLength = -1;
10708 for (InputFilter filter: getFilters()) {
10709 if (filter instanceof InputFilter.LengthFilter) {
10710 maxLength = ((InputFilter.LengthFilter) filter).getMax();
10711 break;
10712 }
10713 }
10714 structure.setMaxTextLength(maxLength);
Justin Klaassen10d07c82017-09-15 17:58:39 -040010715 }
10716 }
10717 structure.setHint(getHint());
10718 structure.setInputType(getInputType());
10719 }
10720
10721 boolean canRequestAutofill() {
10722 if (!isAutofillable()) {
10723 return false;
10724 }
10725 final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
10726 if (afm != null) {
10727 return afm.isEnabled();
10728 }
10729 return false;
10730 }
10731
10732 private void requestAutofill() {
10733 final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
10734 if (afm != null) {
10735 afm.requestAutofill(this);
10736 }
10737 }
10738
10739 @Override
10740 public void autofill(AutofillValue value) {
10741 if (!value.isText() || !isTextEditable()) {
10742 Log.w(LOG_TAG, value + " could not be autofilled into " + this);
10743 return;
10744 }
10745
10746 final CharSequence autofilledValue = value.getTextValue();
10747
10748 // First autofill it...
10749 setText(autofilledValue, mBufferType, true, 0);
10750
10751 // ...then move cursor to the end.
10752 final CharSequence text = getText();
10753 if ((text instanceof Spannable)) {
10754 Selection.setSelection((Spannable) text, text.length());
10755 }
10756 }
10757
10758 @Override
10759 public @AutofillType int getAutofillType() {
10760 return isTextEditable() ? AUTOFILL_TYPE_TEXT : AUTOFILL_TYPE_NONE;
10761 }
10762
10763 /**
10764 * Gets the {@link TextView}'s current text for AutoFill. The value is trimmed to 100K
10765 * {@code char}s if longer.
10766 *
10767 * @return current text, {@code null} if the text is not editable
10768 *
10769 * @see View#getAutofillValue()
10770 */
10771 @Override
10772 @Nullable
10773 public AutofillValue getAutofillValue() {
10774 if (isTextEditable()) {
10775 final CharSequence text = TextUtils.trimToParcelableSize(getText());
10776 return AutofillValue.forText(text);
10777 }
10778 return null;
10779 }
10780
10781 /** @hide */
10782 @Override
10783 public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
10784 super.onInitializeAccessibilityEventInternal(event);
10785
10786 final boolean isPassword = hasPasswordTransformationMethod();
10787 event.setPassword(isPassword);
10788
10789 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
10790 event.setFromIndex(Selection.getSelectionStart(mText));
10791 event.setToIndex(Selection.getSelectionEnd(mText));
10792 event.setItemCount(mText.length());
10793 }
10794 }
10795
10796 /** @hide */
10797 @Override
10798 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
10799 super.onInitializeAccessibilityNodeInfoInternal(info);
10800
10801 final boolean isPassword = hasPasswordTransformationMethod();
10802 info.setPassword(isPassword);
10803 info.setText(getTextForAccessibility());
10804 info.setHintText(mHint);
10805 info.setShowingHintText(isShowingHint());
10806
10807 if (mBufferType == BufferType.EDITABLE) {
10808 info.setEditable(true);
10809 if (isEnabled()) {
10810 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
10811 }
10812 }
10813
10814 if (mEditor != null) {
10815 info.setInputType(mEditor.mInputType);
10816
10817 if (mEditor.mError != null) {
10818 info.setContentInvalid(true);
10819 info.setError(mEditor.mError);
10820 }
10821 }
10822
10823 if (!TextUtils.isEmpty(mText)) {
10824 info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
10825 info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
10826 info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
10827 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
10828 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE
10829 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH
10830 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE);
10831 info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
10832 info.setAvailableExtraData(
10833 Arrays.asList(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY));
10834 }
10835
10836 if (isFocused()) {
10837 if (canCopy()) {
10838 info.addAction(AccessibilityNodeInfo.ACTION_COPY);
10839 }
10840 if (canPaste()) {
10841 info.addAction(AccessibilityNodeInfo.ACTION_PASTE);
10842 }
10843 if (canCut()) {
10844 info.addAction(AccessibilityNodeInfo.ACTION_CUT);
10845 }
10846 if (canShare()) {
10847 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
10848 ACCESSIBILITY_ACTION_SHARE,
10849 getResources().getString(com.android.internal.R.string.share)));
10850 }
10851 if (canProcessText()) { // also implies mEditor is not null.
10852 mEditor.mProcessTextIntentActionsHandler.onInitializeAccessibilityNodeInfo(info);
10853 }
10854 }
10855
10856 // Check for known input filter types.
10857 final int numFilters = mFilters.length;
10858 for (int i = 0; i < numFilters; i++) {
10859 final InputFilter filter = mFilters[i];
10860 if (filter instanceof InputFilter.LengthFilter) {
10861 info.setMaxTextLength(((InputFilter.LengthFilter) filter).getMax());
10862 }
10863 }
10864
10865 if (!isSingleLine()) {
10866 info.setMultiLine(true);
10867 }
10868 }
10869
10870 @Override
10871 public void addExtraDataToAccessibilityNodeInfo(
10872 AccessibilityNodeInfo info, String extraDataKey, Bundle arguments) {
10873 // The only extra data we support requires arguments.
10874 if (arguments == null) {
10875 return;
10876 }
10877 if (extraDataKey.equals(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY)) {
10878 int positionInfoStartIndex = arguments.getInt(
10879 EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, -1);
10880 int positionInfoLength = arguments.getInt(
10881 EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, -1);
10882 if ((positionInfoLength <= 0) || (positionInfoStartIndex < 0)
10883 || (positionInfoStartIndex >= mText.length())) {
10884 Log.e(LOG_TAG, "Invalid arguments for accessibility character locations");
10885 return;
10886 }
10887 RectF[] boundingRects = new RectF[positionInfoLength];
10888 final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
10889 populateCharacterBounds(builder, positionInfoStartIndex,
10890 positionInfoStartIndex + positionInfoLength,
10891 viewportToContentHorizontalOffset(), viewportToContentVerticalOffset());
10892 CursorAnchorInfo cursorAnchorInfo = builder.setMatrix(null).build();
10893 for (int i = 0; i < positionInfoLength; i++) {
10894 int flags = cursorAnchorInfo.getCharacterBoundsFlags(positionInfoStartIndex + i);
10895 if ((flags & FLAG_HAS_VISIBLE_REGION) == FLAG_HAS_VISIBLE_REGION) {
10896 RectF bounds = cursorAnchorInfo
10897 .getCharacterBounds(positionInfoStartIndex + i);
10898 if (bounds != null) {
10899 mapRectFromViewToScreenCoords(bounds, true);
10900 boundingRects[i] = bounds;
10901 }
10902 }
10903 }
10904 info.getExtras().putParcelableArray(extraDataKey, boundingRects);
10905 }
10906 }
10907
10908 /**
10909 * Populate requested character bounds in a {@link CursorAnchorInfo.Builder}
10910 *
10911 * @param builder The builder to populate
10912 * @param startIndex The starting character index to populate
10913 * @param endIndex The ending character index to populate
10914 * @param viewportToContentHorizontalOffset The horizontal offset from the viewport to the
10915 * content
10916 * @param viewportToContentVerticalOffset The vertical offset from the viewport to the content
10917 * @hide
10918 */
10919 public void populateCharacterBounds(CursorAnchorInfo.Builder builder,
10920 int startIndex, int endIndex, float viewportToContentHorizontalOffset,
10921 float viewportToContentVerticalOffset) {
10922 final int minLine = mLayout.getLineForOffset(startIndex);
10923 final int maxLine = mLayout.getLineForOffset(endIndex - 1);
10924 for (int line = minLine; line <= maxLine; ++line) {
10925 final int lineStart = mLayout.getLineStart(line);
10926 final int lineEnd = mLayout.getLineEnd(line);
10927 final int offsetStart = Math.max(lineStart, startIndex);
10928 final int offsetEnd = Math.min(lineEnd, endIndex);
10929 final boolean ltrLine =
10930 mLayout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
10931 final float[] widths = new float[offsetEnd - offsetStart];
Justin Klaassen4d01eea2018-04-03 23:21:57 -040010932 mLayout.getPaint().getTextWidths(mTransformed, offsetStart, offsetEnd, widths);
Justin Klaassen10d07c82017-09-15 17:58:39 -040010933 final float top = mLayout.getLineTop(line);
10934 final float bottom = mLayout.getLineBottom(line);
10935 for (int offset = offsetStart; offset < offsetEnd; ++offset) {
10936 final float charWidth = widths[offset - offsetStart];
10937 final boolean isRtl = mLayout.isRtlCharAt(offset);
10938 final float primary = mLayout.getPrimaryHorizontal(offset);
10939 final float secondary = mLayout.getSecondaryHorizontal(offset);
10940 // TODO: This doesn't work perfectly for text with custom styles and
10941 // TAB chars.
10942 final float left;
10943 final float right;
10944 if (ltrLine) {
10945 if (isRtl) {
10946 left = secondary - charWidth;
10947 right = secondary;
10948 } else {
10949 left = primary;
10950 right = primary + charWidth;
10951 }
10952 } else {
10953 if (!isRtl) {
10954 left = secondary;
10955 right = secondary + charWidth;
10956 } else {
10957 left = primary - charWidth;
10958 right = primary;
10959 }
10960 }
10961 // TODO: Check top-right and bottom-left as well.
10962 final float localLeft = left + viewportToContentHorizontalOffset;
10963 final float localRight = right + viewportToContentHorizontalOffset;
10964 final float localTop = top + viewportToContentVerticalOffset;
10965 final float localBottom = bottom + viewportToContentVerticalOffset;
10966 final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
10967 final boolean isBottomRightVisible =
10968 isPositionVisible(localRight, localBottom);
10969 int characterBoundsFlags = 0;
10970 if (isTopLeftVisible || isBottomRightVisible) {
10971 characterBoundsFlags |= FLAG_HAS_VISIBLE_REGION;
10972 }
10973 if (!isTopLeftVisible || !isBottomRightVisible) {
10974 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
10975 }
10976 if (isRtl) {
10977 characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
10978 }
10979 // Here offset is the index in Java chars.
10980 builder.addCharacterBounds(offset, localLeft, localTop, localRight,
10981 localBottom, characterBoundsFlags);
10982 }
10983 }
10984 }
10985
10986 /**
10987 * @hide
10988 */
10989 public boolean isPositionVisible(final float positionX, final float positionY) {
10990 synchronized (TEMP_POSITION) {
10991 final float[] position = TEMP_POSITION;
10992 position[0] = positionX;
10993 position[1] = positionY;
10994 View view = this;
10995
10996 while (view != null) {
10997 if (view != this) {
10998 // Local scroll is already taken into account in positionX/Y
10999 position[0] -= view.getScrollX();
11000 position[1] -= view.getScrollY();
11001 }
11002
11003 if (position[0] < 0 || position[1] < 0 || position[0] > view.getWidth()
11004 || position[1] > view.getHeight()) {
11005 return false;
11006 }
11007
11008 if (!view.getMatrix().isIdentity()) {
11009 view.getMatrix().mapPoints(position);
11010 }
11011
11012 position[0] += view.getLeft();
11013 position[1] += view.getTop();
11014
11015 final ViewParent parent = view.getParent();
11016 if (parent instanceof View) {
11017 view = (View) parent;
11018 } else {
11019 // We've reached the ViewRoot, stop iterating
11020 view = null;
11021 }
11022 }
11023 }
11024
11025 // We've been able to walk up the view hierarchy and the position was never clipped
11026 return true;
11027 }
11028
11029 /**
11030 * Performs an accessibility action after it has been offered to the
11031 * delegate.
11032 *
11033 * @hide
11034 */
11035 @Override
11036 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
11037 if (mEditor != null
11038 && mEditor.mProcessTextIntentActionsHandler.performAccessibilityAction(action)) {
11039 return true;
11040 }
11041 switch (action) {
11042 case AccessibilityNodeInfo.ACTION_CLICK: {
11043 return performAccessibilityActionClick(arguments);
11044 }
11045 case AccessibilityNodeInfo.ACTION_COPY: {
11046 if (isFocused() && canCopy()) {
11047 if (onTextContextMenuItem(ID_COPY)) {
11048 return true;
11049 }
11050 }
11051 } return false;
11052 case AccessibilityNodeInfo.ACTION_PASTE: {
11053 if (isFocused() && canPaste()) {
11054 if (onTextContextMenuItem(ID_PASTE)) {
11055 return true;
11056 }
11057 }
11058 } return false;
11059 case AccessibilityNodeInfo.ACTION_CUT: {
11060 if (isFocused() && canCut()) {
11061 if (onTextContextMenuItem(ID_CUT)) {
11062 return true;
11063 }
11064 }
11065 } return false;
11066 case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
11067 ensureIterableTextForAccessibilitySelectable();
11068 CharSequence text = getIterableTextForAccessibility();
11069 if (text == null) {
11070 return false;
11071 }
11072 final int start = (arguments != null) ? arguments.getInt(
11073 AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, -1) : -1;
11074 final int end = (arguments != null) ? arguments.getInt(
11075 AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, -1) : -1;
11076 if ((getSelectionStart() != start || getSelectionEnd() != end)) {
11077 // No arguments clears the selection.
11078 if (start == end && end == -1) {
11079 Selection.removeSelection((Spannable) text);
11080 return true;
11081 }
11082 if (start >= 0 && start <= end && end <= text.length()) {
11083 Selection.setSelection((Spannable) text, start, end);
11084 // Make sure selection mode is engaged.
11085 if (mEditor != null) {
11086 mEditor.startSelectionActionModeAsync(false);
11087 }
11088 return true;
11089 }
11090 }
11091 } return false;
11092 case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
11093 case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
11094 ensureIterableTextForAccessibilitySelectable();
11095 return super.performAccessibilityActionInternal(action, arguments);
11096 }
11097 case ACCESSIBILITY_ACTION_SHARE: {
11098 if (isFocused() && canShare()) {
11099 if (onTextContextMenuItem(ID_SHARE)) {
11100 return true;
11101 }
11102 }
11103 } return false;
11104 case AccessibilityNodeInfo.ACTION_SET_TEXT: {
11105 if (!isEnabled() || (mBufferType != BufferType.EDITABLE)) {
11106 return false;
11107 }
11108 CharSequence text = (arguments != null) ? arguments.getCharSequence(
11109 AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE) : null;
11110 setText(text);
11111 if (mText != null) {
11112 int updatedTextLength = mText.length();
11113 if (updatedTextLength > 0) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011114 Selection.setSelection(mSpannable, updatedTextLength);
Justin Klaassen10d07c82017-09-15 17:58:39 -040011115 }
11116 }
11117 } return true;
11118 default: {
11119 return super.performAccessibilityActionInternal(action, arguments);
11120 }
11121 }
11122 }
11123
11124 private boolean performAccessibilityActionClick(Bundle arguments) {
11125 boolean handled = false;
11126
11127 if (!isEnabled()) {
11128 return false;
11129 }
11130
11131 if (isClickable() || isLongClickable()) {
11132 // Simulate View.onTouchEvent for an ACTION_UP event
11133 if (isFocusable() && !isFocused()) {
11134 requestFocus();
11135 }
11136
11137 performClick();
11138 handled = true;
11139 }
11140
11141 // Show the IME, except when selecting in read-only text.
11142 if ((mMovement != null || onCheckIsTextEditor()) && hasSpannableText() && mLayout != null
11143 && (isTextEditable() || isTextSelectable()) && isFocused()) {
11144 final InputMethodManager imm = InputMethodManager.peekInstance();
11145 viewClicked(imm);
11146 if (!isTextSelectable() && mEditor.mShowSoftInputOnFocus && imm != null) {
11147 handled |= imm.showSoftInput(this, 0);
11148 }
11149 }
11150
11151 return handled;
11152 }
11153
11154 private boolean hasSpannableText() {
11155 return mText != null && mText instanceof Spannable;
11156 }
11157
11158 /** @hide */
11159 @Override
11160 public void sendAccessibilityEventInternal(int eventType) {
11161 if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED && mEditor != null) {
11162 mEditor.mProcessTextIntentActionsHandler.initializeAccessibilityActions();
11163 }
11164
11165 super.sendAccessibilityEventInternal(eventType);
11166 }
11167
11168 @Override
11169 public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
11170 // Do not send scroll events since first they are not interesting for
11171 // accessibility and second such events a generated too frequently.
11172 // For details see the implementation of bringTextIntoView().
11173 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
11174 return;
11175 }
11176 super.sendAccessibilityEventUnchecked(event);
11177 }
11178
11179 /**
11180 * Returns the text that should be exposed to accessibility services.
11181 * <p>
11182 * This approximates what is displayed visually. If the user has specified
11183 * that accessibility services should speak passwords, this method will
11184 * bypass any password transformation method and return unobscured text.
11185 *
11186 * @return the text that should be exposed to accessibility services, may
11187 * be {@code null} if no text is set
11188 */
11189 @Nullable
11190 private CharSequence getTextForAccessibility() {
11191 // If the text is empty, we must be showing the hint text.
11192 if (TextUtils.isEmpty(mText)) {
11193 return mHint;
11194 }
11195
11196 // Otherwise, return whatever text is being displayed.
11197 return TextUtils.trimToParcelableSize(mTransformed);
11198 }
11199
11200 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
11201 int fromIndex, int removedCount, int addedCount) {
Justin Klaassen10d07c82017-09-15 17:58:39 -040011202 AccessibilityEvent event =
11203 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
11204 event.setFromIndex(fromIndex);
11205 event.setRemovedCount(removedCount);
11206 event.setAddedCount(addedCount);
11207 event.setBeforeText(beforeText);
11208 sendAccessibilityEventUnchecked(event);
11209 }
11210
11211 /**
11212 * Returns whether this text view is a current input method target. The
11213 * default implementation just checks with {@link InputMethodManager}.
11214 * @return True if the TextView is a current input method target; false otherwise.
11215 */
11216 public boolean isInputMethodTarget() {
11217 InputMethodManager imm = InputMethodManager.peekInstance();
11218 return imm != null && imm.isActive(this);
11219 }
11220
11221 static final int ID_SELECT_ALL = android.R.id.selectAll;
11222 static final int ID_UNDO = android.R.id.undo;
11223 static final int ID_REDO = android.R.id.redo;
11224 static final int ID_CUT = android.R.id.cut;
11225 static final int ID_COPY = android.R.id.copy;
11226 static final int ID_PASTE = android.R.id.paste;
11227 static final int ID_SHARE = android.R.id.shareText;
11228 static final int ID_PASTE_AS_PLAIN_TEXT = android.R.id.pasteAsPlainText;
11229 static final int ID_REPLACE = android.R.id.replaceText;
11230 static final int ID_ASSIST = android.R.id.textAssist;
11231 static final int ID_AUTOFILL = android.R.id.autofill;
11232
11233 /**
11234 * Called when a context menu option for the text view is selected. Currently
11235 * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
11236 * {@link android.R.id#copy}, {@link android.R.id#paste} or {@link android.R.id#shareText}.
11237 *
11238 * @return true if the context menu item action was performed.
11239 */
11240 public boolean onTextContextMenuItem(int id) {
11241 int min = 0;
11242 int max = mText.length();
11243
11244 if (isFocused()) {
11245 final int selStart = getSelectionStart();
11246 final int selEnd = getSelectionEnd();
11247
11248 min = Math.max(0, Math.min(selStart, selEnd));
11249 max = Math.max(0, Math.max(selStart, selEnd));
11250 }
11251
11252 switch (id) {
11253 case ID_SELECT_ALL:
11254 final boolean hadSelection = hasSelection();
11255 selectAllText();
11256 if (mEditor != null && hadSelection) {
11257 mEditor.invalidateActionModeAsync();
11258 }
11259 return true;
11260
11261 case ID_UNDO:
11262 if (mEditor != null) {
11263 mEditor.undo();
11264 }
11265 return true; // Returns true even if nothing was undone.
11266
11267 case ID_REDO:
11268 if (mEditor != null) {
11269 mEditor.redo();
11270 }
11271 return true; // Returns true even if nothing was undone.
11272
11273 case ID_PASTE:
11274 paste(min, max, true /* withFormatting */);
11275 return true;
11276
11277 case ID_PASTE_AS_PLAIN_TEXT:
11278 paste(min, max, false /* withFormatting */);
11279 return true;
11280
11281 case ID_CUT:
11282 final ClipData cutData = ClipData.newPlainText(null, getTransformedText(min, max));
11283 if (setPrimaryClip(cutData)) {
11284 deleteText_internal(min, max);
11285 } else {
11286 Toast.makeText(getContext(),
11287 com.android.internal.R.string.failed_to_copy_to_clipboard,
11288 Toast.LENGTH_SHORT).show();
11289 }
11290 return true;
11291
11292 case ID_COPY:
Jeff Davidsona192cc22018-02-08 15:30:06 -080011293 // For link action mode in a non-selectable/non-focusable TextView,
11294 // make sure that we set the appropriate min/max.
11295 final int selStart = getSelectionStart();
11296 final int selEnd = getSelectionEnd();
11297 min = Math.max(0, Math.min(selStart, selEnd));
11298 max = Math.max(0, Math.max(selStart, selEnd));
Justin Klaassen10d07c82017-09-15 17:58:39 -040011299 final ClipData copyData = ClipData.newPlainText(null, getTransformedText(min, max));
11300 if (setPrimaryClip(copyData)) {
11301 stopTextActionMode();
11302 } else {
11303 Toast.makeText(getContext(),
11304 com.android.internal.R.string.failed_to_copy_to_clipboard,
11305 Toast.LENGTH_SHORT).show();
11306 }
11307 return true;
11308
11309 case ID_REPLACE:
11310 if (mEditor != null) {
11311 mEditor.replace();
11312 }
11313 return true;
11314
11315 case ID_SHARE:
11316 shareSelectedText();
11317 return true;
11318
11319 case ID_AUTOFILL:
11320 requestAutofill();
11321 stopTextActionMode();
11322 return true;
11323 }
11324 return false;
11325 }
11326
11327 CharSequence getTransformedText(int start, int end) {
11328 return removeSuggestionSpans(mTransformed.subSequence(start, end));
11329 }
11330
11331 @Override
11332 public boolean performLongClick() {
11333 boolean handled = false;
11334 boolean performedHapticFeedback = false;
11335
11336 if (mEditor != null) {
11337 mEditor.mIsBeingLongClicked = true;
11338 }
11339
11340 if (super.performLongClick()) {
11341 handled = true;
11342 performedHapticFeedback = true;
11343 }
11344
11345 if (mEditor != null) {
11346 handled |= mEditor.performLongClick(handled);
11347 mEditor.mIsBeingLongClicked = false;
11348 }
11349
11350 if (handled) {
11351 if (!performedHapticFeedback) {
11352 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
11353 }
11354 if (mEditor != null) mEditor.mDiscardNextActionUp = true;
11355 } else {
11356 MetricsLogger.action(
11357 mContext,
11358 MetricsEvent.TEXT_LONGPRESS,
11359 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
11360 }
11361
11362 return handled;
11363 }
11364
11365 @Override
11366 protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
11367 super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
11368 if (mEditor != null) {
11369 mEditor.onScrollChanged();
11370 }
11371 }
11372
11373 /**
11374 * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
11375 * by the IME or by the spell checker as the user types. This is done by adding
11376 * {@link SuggestionSpan}s to the text.
11377 *
11378 * When suggestions are enabled (default), this list of suggestions will be displayed when the
11379 * user asks for them on these parts of the text. This value depends on the inputType of this
11380 * TextView.
11381 *
11382 * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}.
11383 *
11384 * In addition, the type variation must be one of
11385 * {@link InputType#TYPE_TEXT_VARIATION_NORMAL},
11386 * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
11387 * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
11388 * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
11389 * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
11390 *
11391 * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set.
11392 *
11393 * @return true if the suggestions popup window is enabled, based on the inputType.
11394 */
11395 public boolean isSuggestionsEnabled() {
11396 if (mEditor == null) return false;
11397 if ((mEditor.mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) {
11398 return false;
11399 }
11400 if ((mEditor.mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false;
11401
11402 final int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
11403 return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL
11404 || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT
11405 || variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE
11406 || variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE
11407 || variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
11408 }
11409
11410 /**
11411 * If provided, this ActionMode.Callback will be used to create the ActionMode when text
11412 * selection is initiated in this View.
11413 *
11414 * <p>The standard implementation populates the menu with a subset of Select All, Cut, Copy,
11415 * Paste, Replace and Share actions, depending on what this View supports.
11416 *
11417 * <p>A custom implementation can add new entries in the default menu in its
11418 * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, android.view.Menu)}
11419 * method. The default actions can also be removed from the menu using
11420 * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll},
11421 * {@link android.R.id#cut}, {@link android.R.id#copy}, {@link android.R.id#paste},
11422 * {@link android.R.id#replaceText} or {@link android.R.id#shareText} ids as parameters.
11423 *
11424 * <p>Returning false from
11425 * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, android.view.Menu)}
11426 * will prevent the action mode from being started.
11427 *
11428 * <p>Action click events should be handled by the custom implementation of
11429 * {@link android.view.ActionMode.Callback#onActionItemClicked(ActionMode,
11430 * android.view.MenuItem)}.
11431 *
11432 * <p>Note that text selection mode is not started when a TextView receives focus and the
11433 * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
11434 * that case, to allow for quick replacement.
11435 */
11436 public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
11437 createEditorIfNeeded();
11438 mEditor.mCustomSelectionActionModeCallback = actionModeCallback;
11439 }
11440
11441 /**
11442 * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
11443 *
11444 * @return The current custom selection callback.
11445 */
11446 public ActionMode.Callback getCustomSelectionActionModeCallback() {
11447 return mEditor == null ? null : mEditor.mCustomSelectionActionModeCallback;
11448 }
11449
11450 /**
11451 * If provided, this ActionMode.Callback will be used to create the ActionMode when text
11452 * insertion is initiated in this View.
11453 * The standard implementation populates the menu with a subset of Select All,
11454 * Paste and Replace actions, depending on what this View supports.
11455 *
11456 * <p>A custom implementation can add new entries in the default menu in its
11457 * {@link android.view.ActionMode.Callback#onPrepareActionMode(android.view.ActionMode,
11458 * android.view.Menu)} method. The default actions can also be removed from the menu using
11459 * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll},
11460 * {@link android.R.id#paste} or {@link android.R.id#replaceText} ids as parameters.</p>
11461 *
11462 * <p>Returning false from
11463 * {@link android.view.ActionMode.Callback#onCreateActionMode(android.view.ActionMode,
11464 * android.view.Menu)} will prevent the action mode from being started.</p>
11465 *
11466 * <p>Action click events should be handled by the custom implementation of
11467 * {@link android.view.ActionMode.Callback#onActionItemClicked(android.view.ActionMode,
11468 * android.view.MenuItem)}.</p>
11469 *
11470 * <p>Note that text insertion mode is not started when a TextView receives focus and the
11471 * {@link android.R.attr#selectAllOnFocus} flag has been set.</p>
11472 */
11473 public void setCustomInsertionActionModeCallback(ActionMode.Callback actionModeCallback) {
11474 createEditorIfNeeded();
11475 mEditor.mCustomInsertionActionModeCallback = actionModeCallback;
11476 }
11477
11478 /**
11479 * Retrieves the value set in {@link #setCustomInsertionActionModeCallback}. Default is null.
11480 *
11481 * @return The current custom insertion callback.
11482 */
11483 public ActionMode.Callback getCustomInsertionActionModeCallback() {
11484 return mEditor == null ? null : mEditor.mCustomInsertionActionModeCallback;
11485 }
11486
11487 /**
11488 * Sets the {@link TextClassifier} for this TextView.
11489 */
11490 public void setTextClassifier(@Nullable TextClassifier textClassifier) {
11491 mTextClassifier = textClassifier;
11492 }
11493
11494 /**
11495 * Returns the {@link TextClassifier} used by this TextView.
11496 * If no TextClassifier has been set, this TextView uses the default set by the
11497 * {@link TextClassificationManager}.
11498 */
11499 @NonNull
11500 public TextClassifier getTextClassifier() {
11501 if (mTextClassifier == null) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011502 final TextClassificationManager tcm =
Justin Klaassen10d07c82017-09-15 17:58:39 -040011503 mContext.getSystemService(TextClassificationManager.class);
11504 if (tcm != null) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011505 return tcm.getTextClassifier();
Justin Klaassen10d07c82017-09-15 17:58:39 -040011506 }
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011507 return TextClassifier.NO_OP;
Justin Klaassen10d07c82017-09-15 17:58:39 -040011508 }
11509 return mTextClassifier;
11510 }
11511
11512 /**
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011513 * Returns a session-aware text classifier.
11514 * This method creates one if none already exists or the current one is destroyed.
11515 */
11516 @NonNull
11517 TextClassifier getTextClassificationSession() {
11518 if (mTextClassificationSession == null || mTextClassificationSession.isDestroyed()) {
11519 final TextClassificationManager tcm =
11520 mContext.getSystemService(TextClassificationManager.class);
11521 if (tcm != null) {
11522 final String widgetType;
11523 if (isTextEditable()) {
11524 widgetType = TextClassifier.WIDGET_TYPE_EDITTEXT;
11525 } else if (isTextSelectable()) {
11526 widgetType = TextClassifier.WIDGET_TYPE_TEXTVIEW;
11527 } else {
11528 widgetType = TextClassifier.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW;
11529 }
11530 // TODO: Tagged this widgetType with a * so it we can monitor if it reports
11531 // SelectionEvents exactly as the older Logger does. Remove once investigations
11532 // are complete.
11533 final TextClassificationContext textClassificationContext =
11534 new TextClassificationContext.Builder(
11535 mContext.getPackageName(), "*" + widgetType)
11536 .build();
11537 if (mTextClassifier != null) {
11538 mTextClassificationSession = tcm.createTextClassificationSession(
11539 textClassificationContext, mTextClassifier);
11540 } else {
11541 mTextClassificationSession = tcm.createTextClassificationSession(
11542 textClassificationContext);
11543 }
11544 } else {
11545 mTextClassificationSession = TextClassifier.NO_OP;
11546 }
11547 }
11548 return mTextClassificationSession;
11549 }
11550
11551 /**
11552 * Returns true if this TextView uses a no-op TextClassifier.
11553 */
11554 boolean usesNoOpTextClassifier() {
11555 return getTextClassifier() == TextClassifier.NO_OP;
11556 }
11557
11558
11559 /**
11560 * Starts an ActionMode for the specified TextLinkSpan.
Justin Klaassen98fe7812018-01-03 13:39:41 -050011561 *
11562 * @return Whether or not we're attempting to start the action mode.
11563 * @hide
11564 */
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011565 public boolean requestActionMode(@NonNull TextLinks.TextLinkSpan clickedSpan) {
11566 Preconditions.checkNotNull(clickedSpan);
11567
11568 if (!(mText instanceof Spanned)) {
11569 return false;
11570 }
11571
11572 final int start = ((Spanned) mText).getSpanStart(clickedSpan);
11573 final int end = ((Spanned) mText).getSpanEnd(clickedSpan);
11574
11575 if (start < 0 || end > mText.length() || start >= end) {
11576 return false;
11577 }
11578
Jeff Davidsona192cc22018-02-08 15:30:06 -080011579 createEditorIfNeeded();
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011580 mEditor.startLinkActionModeAsync(start, end);
Jeff Davidsona192cc22018-02-08 15:30:06 -080011581 return true;
Justin Klaassen98fe7812018-01-03 13:39:41 -050011582 }
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011583
11584 /**
11585 * Handles a click on the specified TextLinkSpan.
11586 *
11587 * @return Whether or not the click is being handled.
11588 * @hide
11589 */
11590 public boolean handleClick(@NonNull TextLinks.TextLinkSpan clickedSpan) {
11591 Preconditions.checkNotNull(clickedSpan);
11592 if (mText instanceof Spanned) {
11593 final Spanned spanned = (Spanned) mText;
11594 final int start = spanned.getSpanStart(clickedSpan);
11595 final int end = spanned.getSpanEnd(clickedSpan);
11596 if (start >= 0 && end <= mText.length() && start < end) {
11597 final TextClassification.Request request = new TextClassification.Request.Builder(
11598 mText, start, end)
11599 .setDefaultLocales(getTextLocales())
11600 .build();
11601 final Supplier<TextClassification> supplier = () ->
11602 getTextClassifier().classifyText(request);
11603 final Consumer<TextClassification> consumer = classification -> {
11604 if (classification != null) {
11605 if (!classification.getActions().isEmpty()) {
11606 try {
11607 classification.getActions().get(0).getActionIntent().send();
11608 } catch (PendingIntent.CanceledException e) {
11609 Log.e(LOG_TAG, "Error sending PendingIntent", e);
11610 }
11611 } else {
11612 Log.d(LOG_TAG, "No link action to perform");
11613 }
11614 } else {
11615 // classification == null
11616 Log.d(LOG_TAG, "Timeout while classifying text");
11617 }
11618 };
11619 CompletableFuture.supplyAsync(supplier)
11620 .completeOnTimeout(null, 1, TimeUnit.SECONDS)
11621 .thenAccept(consumer);
11622 return true;
11623 }
11624 }
11625 return false;
11626 }
11627
Justin Klaassen98fe7812018-01-03 13:39:41 -050011628 /**
Justin Klaassen10d07c82017-09-15 17:58:39 -040011629 * @hide
11630 */
11631 protected void stopTextActionMode() {
11632 if (mEditor != null) {
11633 mEditor.stopTextActionMode();
11634 }
11635 }
11636
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011637 /** @hide */
11638 public void hideFloatingToolbar(int durationMs) {
11639 if (mEditor != null) {
11640 mEditor.hideFloatingToolbar(durationMs);
11641 }
11642 }
11643
Justin Klaassen10d07c82017-09-15 17:58:39 -040011644 boolean canUndo() {
11645 return mEditor != null && mEditor.canUndo();
11646 }
11647
11648 boolean canRedo() {
11649 return mEditor != null && mEditor.canRedo();
11650 }
11651
11652 boolean canCut() {
11653 if (hasPasswordTransformationMethod()) {
11654 return false;
11655 }
11656
11657 if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null
11658 && mEditor.mKeyListener != null) {
11659 return true;
11660 }
11661
11662 return false;
11663 }
11664
11665 boolean canCopy() {
11666 if (hasPasswordTransformationMethod()) {
11667 return false;
11668 }
11669
11670 if (mText.length() > 0 && hasSelection() && mEditor != null) {
11671 return true;
11672 }
11673
11674 return false;
11675 }
11676
11677 boolean canShare() {
11678 if (!getContext().canStartActivityForResult() || !isDeviceProvisioned()) {
11679 return false;
11680 }
11681 return canCopy();
11682 }
11683
11684 boolean isDeviceProvisioned() {
11685 if (mDeviceProvisionedState == DEVICE_PROVISIONED_UNKNOWN) {
11686 mDeviceProvisionedState = Settings.Global.getInt(
11687 mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0
11688 ? DEVICE_PROVISIONED_YES
11689 : DEVICE_PROVISIONED_NO;
11690 }
11691 return mDeviceProvisionedState == DEVICE_PROVISIONED_YES;
11692 }
11693
11694 boolean canPaste() {
11695 return (mText instanceof Editable
11696 && mEditor != null && mEditor.mKeyListener != null
11697 && getSelectionStart() >= 0
11698 && getSelectionEnd() >= 0
11699 && ((ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE))
11700 .hasPrimaryClip());
11701 }
11702
11703 boolean canPasteAsPlainText() {
11704 if (!canPaste()) {
11705 return false;
11706 }
11707
11708 final ClipData clipData =
11709 ((ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE))
11710 .getPrimaryClip();
11711 final ClipDescription description = clipData.getDescription();
11712 final boolean isPlainType = description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
11713 final CharSequence text = clipData.getItemAt(0).getText();
11714 if (isPlainType && (text instanceof Spanned)) {
11715 Spanned spanned = (Spanned) text;
11716 if (TextUtils.hasStyleSpan(spanned)) {
11717 return true;
11718 }
11719 }
11720 return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
11721 }
11722
11723 boolean canProcessText() {
11724 if (getId() == View.NO_ID) {
11725 return false;
11726 }
11727 return canShare();
11728 }
11729
11730 boolean canSelectAllText() {
11731 return canSelectText() && !hasPasswordTransformationMethod()
11732 && !(getSelectionStart() == 0 && getSelectionEnd() == mText.length());
11733 }
11734
11735 boolean selectAllText() {
11736 if (mEditor != null) {
11737 // Hide the toolbar before changing the selection to avoid flickering.
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011738 hideFloatingToolbar(FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY);
Justin Klaassen10d07c82017-09-15 17:58:39 -040011739 }
11740 final int length = mText.length();
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011741 Selection.setSelection(mSpannable, 0, length);
Justin Klaassen10d07c82017-09-15 17:58:39 -040011742 return length > 0;
11743 }
11744
11745 void replaceSelectionWithText(CharSequence text) {
11746 ((Editable) mText).replace(getSelectionStart(), getSelectionEnd(), text);
11747 }
11748
11749 /**
11750 * Paste clipboard content between min and max positions.
11751 */
11752 private void paste(int min, int max, boolean withFormatting) {
11753 ClipboardManager clipboard =
11754 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
11755 ClipData clip = clipboard.getPrimaryClip();
11756 if (clip != null) {
11757 boolean didFirst = false;
11758 for (int i = 0; i < clip.getItemCount(); i++) {
11759 final CharSequence paste;
11760 if (withFormatting) {
11761 paste = clip.getItemAt(i).coerceToStyledText(getContext());
11762 } else {
11763 // Get an item as text and remove all spans by toString().
11764 final CharSequence text = clip.getItemAt(i).coerceToText(getContext());
11765 paste = (text instanceof Spanned) ? text.toString() : text;
11766 }
11767 if (paste != null) {
11768 if (!didFirst) {
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011769 Selection.setSelection(mSpannable, max);
Justin Klaassen10d07c82017-09-15 17:58:39 -040011770 ((Editable) mText).replace(min, max, paste);
11771 didFirst = true;
11772 } else {
11773 ((Editable) mText).insert(getSelectionEnd(), "\n");
11774 ((Editable) mText).insert(getSelectionEnd(), paste);
11775 }
11776 }
11777 }
11778 sLastCutCopyOrTextChangedTime = 0;
11779 }
11780 }
11781
11782 private void shareSelectedText() {
11783 String selectedText = getSelectedText();
11784 if (selectedText != null && !selectedText.isEmpty()) {
11785 Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
11786 sharingIntent.setType("text/plain");
11787 sharingIntent.removeExtra(android.content.Intent.EXTRA_TEXT);
11788 selectedText = TextUtils.trimToParcelableSize(selectedText);
11789 sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText);
11790 getContext().startActivity(Intent.createChooser(sharingIntent, null));
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011791 Selection.setSelection(mSpannable, getSelectionEnd());
Justin Klaassen10d07c82017-09-15 17:58:39 -040011792 }
11793 }
11794
11795 @CheckResult
11796 private boolean setPrimaryClip(ClipData clip) {
11797 ClipboardManager clipboard =
11798 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
11799 try {
11800 clipboard.setPrimaryClip(clip);
11801 } catch (Throwable t) {
11802 return false;
11803 }
11804 sLastCutCopyOrTextChangedTime = SystemClock.uptimeMillis();
11805 return true;
11806 }
11807
11808 /**
11809 * Get the character offset closest to the specified absolute position. A typical use case is to
11810 * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
11811 *
11812 * @param x The horizontal absolute position of a point on screen
11813 * @param y The vertical absolute position of a point on screen
11814 * @return the character offset for the character whose position is closest to the specified
11815 * position. Returns -1 if there is no layout.
11816 */
11817 public int getOffsetForPosition(float x, float y) {
11818 if (getLayout() == null) return -1;
11819 final int line = getLineAtCoordinate(y);
11820 final int offset = getOffsetAtCoordinate(line, x);
11821 return offset;
11822 }
11823
11824 float convertToLocalHorizontalCoordinate(float x) {
11825 x -= getTotalPaddingLeft();
11826 // Clamp the position to inside of the view.
11827 x = Math.max(0.0f, x);
11828 x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
11829 x += getScrollX();
11830 return x;
11831 }
11832
11833 int getLineAtCoordinate(float y) {
11834 y -= getTotalPaddingTop();
11835 // Clamp the position to inside of the view.
11836 y = Math.max(0.0f, y);
11837 y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
11838 y += getScrollY();
11839 return getLayout().getLineForVertical((int) y);
11840 }
11841
11842 int getLineAtCoordinateUnclamped(float y) {
11843 y -= getTotalPaddingTop();
11844 y += getScrollY();
11845 return getLayout().getLineForVertical((int) y);
11846 }
11847
11848 int getOffsetAtCoordinate(int line, float x) {
11849 x = convertToLocalHorizontalCoordinate(x);
11850 return getLayout().getOffsetForHorizontal(line, x);
11851 }
11852
11853 @Override
11854 public boolean onDragEvent(DragEvent event) {
11855 switch (event.getAction()) {
11856 case DragEvent.ACTION_DRAG_STARTED:
11857 return mEditor != null && mEditor.hasInsertionController();
11858
11859 case DragEvent.ACTION_DRAG_ENTERED:
11860 TextView.this.requestFocus();
11861 return true;
11862
11863 case DragEvent.ACTION_DRAG_LOCATION:
11864 if (mText instanceof Spannable) {
11865 final int offset = getOffsetForPosition(event.getX(), event.getY());
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011866 Selection.setSelection(mSpannable, offset);
Justin Klaassen10d07c82017-09-15 17:58:39 -040011867 }
11868 return true;
11869
11870 case DragEvent.ACTION_DROP:
11871 if (mEditor != null) mEditor.onDrop(event);
11872 return true;
11873
11874 case DragEvent.ACTION_DRAG_ENDED:
11875 case DragEvent.ACTION_DRAG_EXITED:
11876 default:
11877 return true;
11878 }
11879 }
11880
11881 boolean isInBatchEditMode() {
11882 if (mEditor == null) return false;
11883 final Editor.InputMethodState ims = mEditor.mInputMethodState;
11884 if (ims != null) {
11885 return ims.mBatchEditNesting > 0;
11886 }
11887 return mEditor.mInBatchEditControllers;
11888 }
11889
11890 @Override
11891 public void onRtlPropertiesChanged(int layoutDirection) {
11892 super.onRtlPropertiesChanged(layoutDirection);
11893
11894 final TextDirectionHeuristic newTextDir = getTextDirectionHeuristic();
11895 if (mTextDir != newTextDir) {
11896 mTextDir = newTextDir;
11897 if (mLayout != null) {
11898 checkForRelayout();
11899 }
11900 }
11901 }
11902
11903 /**
Justin Klaassen4d01eea2018-04-03 23:21:57 -040011904 * Returns the current {@link TextDirectionHeuristic}.
11905 *
11906 * @return the current {@link TextDirectionHeuristic}.
Justin Klaassen10d07c82017-09-15 17:58:39 -040011907 * @hide
11908 */
11909 protected TextDirectionHeuristic getTextDirectionHeuristic() {
11910 if (hasPasswordTransformationMethod()) {
11911 // passwords fields should be LTR
11912 return TextDirectionHeuristics.LTR;
11913 }
11914
11915 if (mEditor != null
11916 && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
11917 == EditorInfo.TYPE_CLASS_PHONE) {
11918 // Phone numbers must be in the direction of the locale's digits. Most locales have LTR
11919 // digits, but some locales, such as those written in the Adlam or N'Ko scripts, have
11920 // RTL digits.
11921 final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(getTextLocale());
11922 final String zero = symbols.getDigitStrings()[0];
11923 // In case the zero digit is multi-codepoint, just use the first codepoint to determine
11924 // direction.
11925 final int firstCodepoint = zero.codePointAt(0);
11926 final byte digitDirection = Character.getDirectionality(firstCodepoint);
11927 if (digitDirection == Character.DIRECTIONALITY_RIGHT_TO_LEFT
11928 || digitDirection == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) {
11929 return TextDirectionHeuristics.RTL;
11930 } else {
11931 return TextDirectionHeuristics.LTR;
11932 }
11933 }
11934
11935 // Always need to resolve layout direction first
11936 final boolean defaultIsRtl = (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
11937
11938 // Now, we can select the heuristic
11939 switch (getTextDirection()) {
11940 default:
11941 case TEXT_DIRECTION_FIRST_STRONG:
11942 return (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
11943 TextDirectionHeuristics.FIRSTSTRONG_LTR);
11944 case TEXT_DIRECTION_ANY_RTL:
11945 return TextDirectionHeuristics.ANYRTL_LTR;
11946 case TEXT_DIRECTION_LTR:
11947 return TextDirectionHeuristics.LTR;
11948 case TEXT_DIRECTION_RTL:
11949 return TextDirectionHeuristics.RTL;
11950 case TEXT_DIRECTION_LOCALE:
11951 return TextDirectionHeuristics.LOCALE;
11952 case TEXT_DIRECTION_FIRST_STRONG_LTR:
11953 return TextDirectionHeuristics.FIRSTSTRONG_LTR;
11954 case TEXT_DIRECTION_FIRST_STRONG_RTL:
11955 return TextDirectionHeuristics.FIRSTSTRONG_RTL;
11956 }
11957 }
11958
11959 /**
11960 * @hide
11961 */
11962 @Override
11963 public void onResolveDrawables(int layoutDirection) {
11964 // No need to resolve twice
11965 if (mLastLayoutDirection == layoutDirection) {
11966 return;
11967 }
11968 mLastLayoutDirection = layoutDirection;
11969
11970 // Resolve drawables
11971 if (mDrawables != null) {
11972 if (mDrawables.resolveWithLayoutDirection(layoutDirection)) {
11973 prepareDrawableForDisplay(mDrawables.mShowing[Drawables.LEFT]);
11974 prepareDrawableForDisplay(mDrawables.mShowing[Drawables.RIGHT]);
11975 applyCompoundDrawableTint();
11976 }
11977 }
11978 }
11979
11980 /**
11981 * Prepares a drawable for display by propagating layout direction and
11982 * drawable state.
11983 *
11984 * @param dr the drawable to prepare
11985 */
11986 private void prepareDrawableForDisplay(@Nullable Drawable dr) {
11987 if (dr == null) {
11988 return;
11989 }
11990
11991 dr.setLayoutDirection(getLayoutDirection());
11992
11993 if (dr.isStateful()) {
11994 dr.setState(getDrawableState());
11995 dr.jumpToCurrentState();
11996 }
11997 }
11998
11999 /**
12000 * @hide
12001 */
12002 protected void resetResolvedDrawables() {
12003 super.resetResolvedDrawables();
12004 mLastLayoutDirection = -1;
12005 }
12006
12007 /**
12008 * @hide
12009 */
12010 protected void viewClicked(InputMethodManager imm) {
12011 if (imm != null) {
12012 imm.viewClicked(this);
12013 }
12014 }
12015
12016 /**
12017 * Deletes the range of text [start, end[.
12018 * @hide
12019 */
12020 protected void deleteText_internal(int start, int end) {
12021 ((Editable) mText).delete(start, end);
12022 }
12023
12024 /**
12025 * Replaces the range of text [start, end[ by replacement text
12026 * @hide
12027 */
12028 protected void replaceText_internal(int start, int end, CharSequence text) {
12029 ((Editable) mText).replace(start, end, text);
12030 }
12031
12032 /**
12033 * Sets a span on the specified range of text
12034 * @hide
12035 */
12036 protected void setSpan_internal(Object span, int start, int end, int flags) {
12037 ((Editable) mText).setSpan(span, start, end, flags);
12038 }
12039
12040 /**
12041 * Moves the cursor to the specified offset position in text
12042 * @hide
12043 */
12044 protected void setCursorPosition_internal(int start, int end) {
12045 Selection.setSelection(((Editable) mText), start, end);
12046 }
12047
12048 /**
12049 * An Editor should be created as soon as any of the editable-specific fields (grouped
12050 * inside the Editor object) is assigned to a non-default value.
12051 * This method will create the Editor if needed.
12052 *
12053 * A standard TextView (as well as buttons, checkboxes...) should not qualify and hence will
12054 * have a null Editor, unlike an EditText. Inconsistent in-between states will have an
12055 * Editor for backward compatibility, as soon as one of these fields is assigned.
12056 *
12057 * Also note that for performance reasons, the mEditor is created when needed, but not
12058 * reset when no more edit-specific fields are needed.
12059 */
12060 private void createEditorIfNeeded() {
12061 if (mEditor == null) {
12062 mEditor = new Editor(this);
12063 }
12064 }
12065
12066 /**
12067 * @hide
12068 */
12069 @Override
12070 public CharSequence getIterableTextForAccessibility() {
12071 return mText;
12072 }
12073
12074 private void ensureIterableTextForAccessibilitySelectable() {
12075 if (!(mText instanceof Spannable)) {
12076 setText(mText, BufferType.SPANNABLE);
12077 }
12078 }
12079
12080 /**
12081 * @hide
12082 */
12083 @Override
12084 public TextSegmentIterator getIteratorForGranularity(int granularity) {
12085 switch (granularity) {
12086 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE: {
12087 Spannable text = (Spannable) getIterableTextForAccessibility();
12088 if (!TextUtils.isEmpty(text) && getLayout() != null) {
12089 AccessibilityIterators.LineTextSegmentIterator iterator =
12090 AccessibilityIterators.LineTextSegmentIterator.getInstance();
12091 iterator.initialize(text, getLayout());
12092 return iterator;
12093 }
12094 } break;
12095 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE: {
12096 Spannable text = (Spannable) getIterableTextForAccessibility();
12097 if (!TextUtils.isEmpty(text) && getLayout() != null) {
12098 AccessibilityIterators.PageTextSegmentIterator iterator =
12099 AccessibilityIterators.PageTextSegmentIterator.getInstance();
12100 iterator.initialize(this);
12101 return iterator;
12102 }
12103 } break;
12104 }
12105 return super.getIteratorForGranularity(granularity);
12106 }
12107
12108 /**
12109 * @hide
12110 */
12111 @Override
12112 public int getAccessibilitySelectionStart() {
12113 return getSelectionStart();
12114 }
12115
12116 /**
12117 * @hide
12118 */
12119 public boolean isAccessibilitySelectionExtendable() {
12120 return true;
12121 }
12122
12123 /**
12124 * @hide
12125 */
12126 @Override
12127 public int getAccessibilitySelectionEnd() {
12128 return getSelectionEnd();
12129 }
12130
12131 /**
12132 * @hide
12133 */
12134 @Override
12135 public void setAccessibilitySelection(int start, int end) {
12136 if (getAccessibilitySelectionStart() == start
12137 && getAccessibilitySelectionEnd() == end) {
12138 return;
12139 }
12140 CharSequence text = getIterableTextForAccessibility();
12141 if (Math.min(start, end) >= 0 && Math.max(start, end) <= text.length()) {
12142 Selection.setSelection((Spannable) text, start, end);
12143 } else {
12144 Selection.removeSelection((Spannable) text);
12145 }
12146 // Hide all selection controllers used for adjusting selection
12147 // since we are doing so explicitlty by other means and these
12148 // controllers interact with how selection behaves.
12149 if (mEditor != null) {
12150 mEditor.hideCursorAndSpanControllers();
12151 mEditor.stopTextActionMode();
12152 }
12153 }
12154
12155 /** @hide */
12156 @Override
12157 protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
12158 super.encodeProperties(stream);
12159
12160 TruncateAt ellipsize = getEllipsize();
12161 stream.addProperty("text:ellipsize", ellipsize == null ? null : ellipsize.name());
12162 stream.addProperty("text:textSize", getTextSize());
12163 stream.addProperty("text:scaledTextSize", getScaledTextSize());
12164 stream.addProperty("text:typefaceStyle", getTypefaceStyle());
12165 stream.addProperty("text:selectionStart", getSelectionStart());
12166 stream.addProperty("text:selectionEnd", getSelectionEnd());
12167 stream.addProperty("text:curTextColor", mCurTextColor);
12168 stream.addProperty("text:text", mText == null ? null : mText.toString());
12169 stream.addProperty("text:gravity", mGravity);
12170 }
12171
12172 /**
12173 * User interface state that is stored by TextView for implementing
12174 * {@link View#onSaveInstanceState}.
12175 */
12176 public static class SavedState extends BaseSavedState {
12177 int selStart = -1;
12178 int selEnd = -1;
12179 CharSequence text;
12180 boolean frozenWithFocus;
12181 CharSequence error;
12182 ParcelableParcel editorState; // Optional state from Editor.
12183
12184 SavedState(Parcelable superState) {
12185 super(superState);
12186 }
12187
12188 @Override
12189 public void writeToParcel(Parcel out, int flags) {
12190 super.writeToParcel(out, flags);
12191 out.writeInt(selStart);
12192 out.writeInt(selEnd);
12193 out.writeInt(frozenWithFocus ? 1 : 0);
12194 TextUtils.writeToParcel(text, out, flags);
12195
12196 if (error == null) {
12197 out.writeInt(0);
12198 } else {
12199 out.writeInt(1);
12200 TextUtils.writeToParcel(error, out, flags);
12201 }
12202
12203 if (editorState == null) {
12204 out.writeInt(0);
12205 } else {
12206 out.writeInt(1);
12207 editorState.writeToParcel(out, flags);
12208 }
12209 }
12210
12211 @Override
12212 public String toString() {
12213 String str = "TextView.SavedState{"
12214 + Integer.toHexString(System.identityHashCode(this))
12215 + " start=" + selStart + " end=" + selEnd;
12216 if (text != null) {
12217 str += " text=" + text;
12218 }
12219 return str + "}";
12220 }
12221
12222 @SuppressWarnings("hiding")
12223 public static final Parcelable.Creator<SavedState> CREATOR =
12224 new Parcelable.Creator<SavedState>() {
12225 public SavedState createFromParcel(Parcel in) {
12226 return new SavedState(in);
12227 }
12228
12229 public SavedState[] newArray(int size) {
12230 return new SavedState[size];
12231 }
12232 };
12233
12234 private SavedState(Parcel in) {
12235 super(in);
12236 selStart = in.readInt();
12237 selEnd = in.readInt();
12238 frozenWithFocus = (in.readInt() != 0);
12239 text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
12240
12241 if (in.readInt() != 0) {
12242 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
12243 }
12244
12245 if (in.readInt() != 0) {
12246 editorState = ParcelableParcel.CREATOR.createFromParcel(in);
12247 }
12248 }
12249 }
12250
12251 private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations {
12252 private char[] mChars;
12253 private int mStart, mLength;
12254
12255 public CharWrapper(char[] chars, int start, int len) {
12256 mChars = chars;
12257 mStart = start;
12258 mLength = len;
12259 }
12260
12261 /* package */ void set(char[] chars, int start, int len) {
12262 mChars = chars;
12263 mStart = start;
12264 mLength = len;
12265 }
12266
12267 public int length() {
12268 return mLength;
12269 }
12270
12271 public char charAt(int off) {
12272 return mChars[off + mStart];
12273 }
12274
12275 @Override
12276 public String toString() {
12277 return new String(mChars, mStart, mLength);
12278 }
12279
12280 public CharSequence subSequence(int start, int end) {
12281 if (start < 0 || end < 0 || start > mLength || end > mLength) {
12282 throw new IndexOutOfBoundsException(start + ", " + end);
12283 }
12284
12285 return new String(mChars, start + mStart, end - start);
12286 }
12287
12288 public void getChars(int start, int end, char[] buf, int off) {
12289 if (start < 0 || end < 0 || start > mLength || end > mLength) {
12290 throw new IndexOutOfBoundsException(start + ", " + end);
12291 }
12292
12293 System.arraycopy(mChars, start + mStart, buf, off, end - start);
12294 }
12295
12296 @Override
12297 public void drawText(BaseCanvas c, int start, int end,
12298 float x, float y, Paint p) {
12299 c.drawText(mChars, start + mStart, end - start, x, y, p);
12300 }
12301
12302 @Override
12303 public void drawTextRun(BaseCanvas c, int start, int end,
12304 int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint p) {
12305 int count = end - start;
12306 int contextCount = contextEnd - contextStart;
12307 c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
12308 contextCount, x, y, isRtl, p);
12309 }
12310
12311 public float measureText(int start, int end, Paint p) {
12312 return p.measureText(mChars, start + mStart, end - start);
12313 }
12314
12315 public int getTextWidths(int start, int end, float[] widths, Paint p) {
12316 return p.getTextWidths(mChars, start + mStart, end - start, widths);
12317 }
12318
12319 public float getTextRunAdvances(int start, int end, int contextStart,
12320 int contextEnd, boolean isRtl, float[] advances, int advancesIndex,
12321 Paint p) {
12322 int count = end - start;
12323 int contextCount = contextEnd - contextStart;
12324 return p.getTextRunAdvances(mChars, start + mStart, count,
12325 contextStart + mStart, contextCount, isRtl, advances,
12326 advancesIndex);
12327 }
12328
12329 public int getTextRunCursor(int contextStart, int contextEnd, int dir,
12330 int offset, int cursorOpt, Paint p) {
12331 int contextCount = contextEnd - contextStart;
12332 return p.getTextRunCursor(mChars, contextStart + mStart,
12333 contextCount, dir, offset + mStart, cursorOpt);
12334 }
12335 }
12336
12337 private static final class Marquee {
12338 // TODO: Add an option to configure this
12339 private static final float MARQUEE_DELTA_MAX = 0.07f;
12340 private static final int MARQUEE_DELAY = 1200;
12341 private static final int MARQUEE_DP_PER_SECOND = 30;
12342
12343 private static final byte MARQUEE_STOPPED = 0x0;
12344 private static final byte MARQUEE_STARTING = 0x1;
12345 private static final byte MARQUEE_RUNNING = 0x2;
12346
12347 private final WeakReference<TextView> mView;
12348 private final Choreographer mChoreographer;
12349
12350 private byte mStatus = MARQUEE_STOPPED;
Jeff Davidsona192cc22018-02-08 15:30:06 -080012351 private final float mPixelsPerMs;
Justin Klaassen10d07c82017-09-15 17:58:39 -040012352 private float mMaxScroll;
12353 private float mMaxFadeScroll;
12354 private float mGhostStart;
12355 private float mGhostOffset;
12356 private float mFadeStop;
12357 private int mRepeatLimit;
12358
12359 private float mScroll;
12360 private long mLastAnimationMs;
12361
12362 Marquee(TextView v) {
12363 final float density = v.getContext().getResources().getDisplayMetrics().density;
Jeff Davidsona192cc22018-02-08 15:30:06 -080012364 mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f;
Justin Klaassen10d07c82017-09-15 17:58:39 -040012365 mView = new WeakReference<TextView>(v);
12366 mChoreographer = Choreographer.getInstance();
12367 }
12368
12369 private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {
12370 @Override
12371 public void doFrame(long frameTimeNanos) {
12372 tick();
12373 }
12374 };
12375
12376 private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
12377 @Override
12378 public void doFrame(long frameTimeNanos) {
12379 mStatus = MARQUEE_RUNNING;
12380 mLastAnimationMs = mChoreographer.getFrameTime();
12381 tick();
12382 }
12383 };
12384
12385 private Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
12386 @Override
12387 public void doFrame(long frameTimeNanos) {
12388 if (mStatus == MARQUEE_RUNNING) {
12389 if (mRepeatLimit >= 0) {
12390 mRepeatLimit--;
12391 }
12392 start(mRepeatLimit);
12393 }
12394 }
12395 };
12396
12397 void tick() {
12398 if (mStatus != MARQUEE_RUNNING) {
12399 return;
12400 }
12401
12402 mChoreographer.removeFrameCallback(mTickCallback);
12403
12404 final TextView textView = mView.get();
12405 if (textView != null && (textView.isFocused() || textView.isSelected())) {
12406 long currentMs = mChoreographer.getFrameTime();
12407 long deltaMs = currentMs - mLastAnimationMs;
12408 mLastAnimationMs = currentMs;
Jeff Davidsona192cc22018-02-08 15:30:06 -080012409 float deltaPx = deltaMs * mPixelsPerMs;
Justin Klaassen10d07c82017-09-15 17:58:39 -040012410 mScroll += deltaPx;
12411 if (mScroll > mMaxScroll) {
12412 mScroll = mMaxScroll;
12413 mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
12414 } else {
12415 mChoreographer.postFrameCallback(mTickCallback);
12416 }
12417 textView.invalidate();
12418 }
12419 }
12420
12421 void stop() {
12422 mStatus = MARQUEE_STOPPED;
12423 mChoreographer.removeFrameCallback(mStartCallback);
12424 mChoreographer.removeFrameCallback(mRestartCallback);
12425 mChoreographer.removeFrameCallback(mTickCallback);
12426 resetScroll();
12427 }
12428
12429 private void resetScroll() {
12430 mScroll = 0.0f;
12431 final TextView textView = mView.get();
12432 if (textView != null) textView.invalidate();
12433 }
12434
12435 void start(int repeatLimit) {
12436 if (repeatLimit == 0) {
12437 stop();
12438 return;
12439 }
12440 mRepeatLimit = repeatLimit;
12441 final TextView textView = mView.get();
12442 if (textView != null && textView.mLayout != null) {
12443 mStatus = MARQUEE_STARTING;
12444 mScroll = 0.0f;
12445 final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft()
12446 - textView.getCompoundPaddingRight();
12447 final float lineWidth = textView.mLayout.getLineWidth(0);
12448 final float gap = textWidth / 3.0f;
12449 mGhostStart = lineWidth - textWidth + gap;
12450 mMaxScroll = mGhostStart + textWidth;
12451 mGhostOffset = lineWidth + gap;
12452 mFadeStop = lineWidth + textWidth / 6.0f;
12453 mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
12454
12455 textView.invalidate();
12456 mChoreographer.postFrameCallback(mStartCallback);
12457 }
12458 }
12459
12460 float getGhostOffset() {
12461 return mGhostOffset;
12462 }
12463
12464 float getScroll() {
12465 return mScroll;
12466 }
12467
12468 float getMaxFadeScroll() {
12469 return mMaxFadeScroll;
12470 }
12471
12472 boolean shouldDrawLeftFade() {
12473 return mScroll <= mFadeStop;
12474 }
12475
12476 boolean shouldDrawGhost() {
12477 return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
12478 }
12479
12480 boolean isRunning() {
12481 return mStatus == MARQUEE_RUNNING;
12482 }
12483
12484 boolean isStopped() {
12485 return mStatus == MARQUEE_STOPPED;
12486 }
12487 }
12488
12489 private class ChangeWatcher implements TextWatcher, SpanWatcher {
12490
12491 private CharSequence mBeforeText;
12492
12493 public void beforeTextChanged(CharSequence buffer, int start,
12494 int before, int after) {
12495 if (DEBUG_EXTRACT) {
12496 Log.v(LOG_TAG, "beforeTextChanged start=" + start
12497 + " before=" + before + " after=" + after + ": " + buffer);
12498 }
12499
Justin Klaassen4d01eea2018-04-03 23:21:57 -040012500 if (AccessibilityManager.getInstance(mContext).isEnabled() && (mTransformed != null)) {
12501 mBeforeText = mTransformed.toString();
Justin Klaassen10d07c82017-09-15 17:58:39 -040012502 }
12503
12504 TextView.this.sendBeforeTextChanged(buffer, start, before, after);
12505 }
12506
12507 public void onTextChanged(CharSequence buffer, int start, int before, int after) {
12508 if (DEBUG_EXTRACT) {
12509 Log.v(LOG_TAG, "onTextChanged start=" + start
12510 + " before=" + before + " after=" + after + ": " + buffer);
12511 }
12512 TextView.this.handleTextChanged(buffer, start, before, after);
12513
12514 if (AccessibilityManager.getInstance(mContext).isEnabled()
12515 && (isFocused() || isSelected() && isShown())) {
12516 sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
12517 mBeforeText = null;
12518 }
12519 }
12520
12521 public void afterTextChanged(Editable buffer) {
12522 if (DEBUG_EXTRACT) {
12523 Log.v(LOG_TAG, "afterTextChanged: " + buffer);
12524 }
12525 TextView.this.sendAfterTextChanged(buffer);
12526
12527 if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
12528 MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
12529 }
12530 }
12531
12532 public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) {
12533 if (DEBUG_EXTRACT) {
12534 Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
12535 + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
12536 }
12537 TextView.this.spanChange(buf, what, s, st, e, en);
12538 }
12539
12540 public void onSpanAdded(Spannable buf, Object what, int s, int e) {
12541 if (DEBUG_EXTRACT) {
12542 Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e + " what=" + what + ": " + buf);
12543 }
12544 TextView.this.spanChange(buf, what, -1, s, -1, e);
12545 }
12546
12547 public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
12548 if (DEBUG_EXTRACT) {
12549 Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e + " what=" + what + ": " + buf);
12550 }
12551 TextView.this.spanChange(buf, what, s, -1, e, -1);
12552 }
12553 }
12554}