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