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