TextView.java revision fc2563ba4e9c93ed803f3ce540f92f199f848036
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 com.android.internal.util.FastMath; 20import com.android.internal.widget.EditableInputConnection; 21 22import org.xmlpull.v1.XmlPullParserException; 23 24import android.content.ClipboardManager; 25import android.content.ClippedData; 26import android.content.Context; 27import android.content.pm.PackageManager; 28import android.content.res.ColorStateList; 29import android.content.res.Resources; 30import android.content.res.TypedArray; 31import android.content.res.XmlResourceParser; 32import android.graphics.Canvas; 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.net.Uri; 40import android.os.Bundle; 41import android.os.Handler; 42import android.os.Message; 43import android.os.Parcel; 44import android.os.Parcelable; 45import android.os.ResultReceiver; 46import android.os.SystemClock; 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.Spanned; 61import android.text.SpannedString; 62import android.text.StaticLayout; 63import android.text.TextPaint; 64import android.text.TextUtils; 65import android.text.TextWatcher; 66import android.text.method.ArrowKeyMovementMethod; 67import android.text.method.DateKeyListener; 68import android.text.method.DateTimeKeyListener; 69import android.text.method.DialerKeyListener; 70import android.text.method.DigitsKeyListener; 71import android.text.method.KeyListener; 72import android.text.method.LinkMovementMethod; 73import android.text.method.MetaKeyKeyListener; 74import android.text.method.MovementMethod; 75import android.text.method.PasswordTransformationMethod; 76import android.text.method.SingleLineTransformationMethod; 77import android.text.method.TextKeyListener; 78import android.text.method.TimeKeyListener; 79import android.text.method.TransformationMethod; 80import android.text.style.ParagraphStyle; 81import android.text.style.URLSpan; 82import android.text.style.UpdateAppearance; 83import android.text.util.Linkify; 84import android.util.AttributeSet; 85import android.util.FloatMath; 86import android.util.Log; 87import android.util.TypedValue; 88import android.view.ActionMode; 89import android.view.ContextMenu; 90import android.view.Gravity; 91import android.view.HapticFeedbackConstants; 92import android.view.KeyEvent; 93import android.view.LayoutInflater; 94import android.view.Menu; 95import android.view.MenuItem; 96import android.view.MotionEvent; 97import android.view.View; 98import android.view.ViewConfiguration; 99import android.view.ViewDebug; 100import android.view.ViewGroup.LayoutParams; 101import android.view.ViewRoot; 102import android.view.ViewTreeObserver; 103import android.view.accessibility.AccessibilityEvent; 104import android.view.accessibility.AccessibilityManager; 105import android.view.animation.AnimationUtils; 106import android.view.inputmethod.BaseInputConnection; 107import android.view.inputmethod.CompletionInfo; 108import android.view.inputmethod.EditorInfo; 109import android.view.inputmethod.ExtractedText; 110import android.view.inputmethod.ExtractedTextRequest; 111import android.view.inputmethod.InputConnection; 112import android.view.inputmethod.InputMethodManager; 113import android.widget.RemoteViews.RemoteView; 114 115import java.io.IOException; 116import java.lang.ref.WeakReference; 117import java.util.ArrayList; 118 119/** 120 * Displays text to the user and optionally allows them to edit it. A TextView 121 * is a complete text editor, however the basic class is configured to not 122 * allow editing; see {@link EditText} for a subclass that configures the text 123 * view for editing. 124 * 125 * <p> 126 * <b>XML attributes</b> 127 * <p> 128 * See {@link android.R.styleable#TextView TextView Attributes}, 129 * {@link android.R.styleable#View View Attributes} 130 * 131 * @attr ref android.R.styleable#TextView_text 132 * @attr ref android.R.styleable#TextView_bufferType 133 * @attr ref android.R.styleable#TextView_hint 134 * @attr ref android.R.styleable#TextView_textColor 135 * @attr ref android.R.styleable#TextView_textColorHighlight 136 * @attr ref android.R.styleable#TextView_textColorHint 137 * @attr ref android.R.styleable#TextView_textAppearance 138 * @attr ref android.R.styleable#TextView_textColorLink 139 * @attr ref android.R.styleable#TextView_textSize 140 * @attr ref android.R.styleable#TextView_textScaleX 141 * @attr ref android.R.styleable#TextView_typeface 142 * @attr ref android.R.styleable#TextView_textStyle 143 * @attr ref android.R.styleable#TextView_cursorVisible 144 * @attr ref android.R.styleable#TextView_maxLines 145 * @attr ref android.R.styleable#TextView_maxHeight 146 * @attr ref android.R.styleable#TextView_lines 147 * @attr ref android.R.styleable#TextView_height 148 * @attr ref android.R.styleable#TextView_minLines 149 * @attr ref android.R.styleable#TextView_minHeight 150 * @attr ref android.R.styleable#TextView_maxEms 151 * @attr ref android.R.styleable#TextView_maxWidth 152 * @attr ref android.R.styleable#TextView_ems 153 * @attr ref android.R.styleable#TextView_width 154 * @attr ref android.R.styleable#TextView_minEms 155 * @attr ref android.R.styleable#TextView_minWidth 156 * @attr ref android.R.styleable#TextView_gravity 157 * @attr ref android.R.styleable#TextView_scrollHorizontally 158 * @attr ref android.R.styleable#TextView_password 159 * @attr ref android.R.styleable#TextView_singleLine 160 * @attr ref android.R.styleable#TextView_selectAllOnFocus 161 * @attr ref android.R.styleable#TextView_includeFontPadding 162 * @attr ref android.R.styleable#TextView_maxLength 163 * @attr ref android.R.styleable#TextView_shadowColor 164 * @attr ref android.R.styleable#TextView_shadowDx 165 * @attr ref android.R.styleable#TextView_shadowDy 166 * @attr ref android.R.styleable#TextView_shadowRadius 167 * @attr ref android.R.styleable#TextView_autoLink 168 * @attr ref android.R.styleable#TextView_linksClickable 169 * @attr ref android.R.styleable#TextView_numeric 170 * @attr ref android.R.styleable#TextView_digits 171 * @attr ref android.R.styleable#TextView_phoneNumber 172 * @attr ref android.R.styleable#TextView_inputMethod 173 * @attr ref android.R.styleable#TextView_capitalize 174 * @attr ref android.R.styleable#TextView_autoText 175 * @attr ref android.R.styleable#TextView_editable 176 * @attr ref android.R.styleable#TextView_freezesText 177 * @attr ref android.R.styleable#TextView_ellipsize 178 * @attr ref android.R.styleable#TextView_drawableTop 179 * @attr ref android.R.styleable#TextView_drawableBottom 180 * @attr ref android.R.styleable#TextView_drawableRight 181 * @attr ref android.R.styleable#TextView_drawableLeft 182 * @attr ref android.R.styleable#TextView_drawablePadding 183 * @attr ref android.R.styleable#TextView_lineSpacingExtra 184 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier 185 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit 186 * @attr ref android.R.styleable#TextView_inputType 187 * @attr ref android.R.styleable#TextView_imeOptions 188 * @attr ref android.R.styleable#TextView_privateImeOptions 189 * @attr ref android.R.styleable#TextView_imeActionLabel 190 * @attr ref android.R.styleable#TextView_imeActionId 191 * @attr ref android.R.styleable#TextView_editorExtras 192 */ 193@RemoteView 194public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { 195 static final String LOG_TAG = "TextView"; 196 static final boolean DEBUG_EXTRACT = false; 197 198 private static final int PRIORITY = 100; 199 200 private int mCurrentAlpha = 255; 201 private ColorStateList mTextColor; 202 private int mCurTextColor; 203 private ColorStateList mHintTextColor; 204 private ColorStateList mLinkTextColor; 205 private int mCurHintTextColor; 206 private boolean mFreezesText; 207 private boolean mFrozenWithFocus; 208 private boolean mTemporaryDetach; 209 private boolean mDispatchTemporaryDetach; 210 211 private boolean mEatTouchRelease = false; 212 private boolean mScrolled = false; 213 214 private Editable.Factory mEditableFactory = Editable.Factory.getInstance(); 215 private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance(); 216 217 private float mShadowRadius, mShadowDx, mShadowDy; 218 219 private static final int PREDRAW_NOT_REGISTERED = 0; 220 private static final int PREDRAW_PENDING = 1; 221 private static final int PREDRAW_DONE = 2; 222 private int mPreDrawState = PREDRAW_NOT_REGISTERED; 223 224 private TextUtils.TruncateAt mEllipsize = null; 225 226 // Enum for the "typeface" XML parameter. 227 // TODO: How can we get this from the XML instead of hardcoding it here? 228 private static final int SANS = 1; 229 private static final int SERIF = 2; 230 private static final int MONOSPACE = 3; 231 232 // Bitfield for the "numeric" XML parameter. 233 // TODO: How can we get this from the XML instead of hardcoding it here? 234 private static final int SIGNED = 2; 235 private static final int DECIMAL = 4; 236 237 class Drawables { 238 final Rect mCompoundRect = new Rect(); 239 Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight; 240 int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; 241 int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; 242 int mDrawablePadding; 243 } 244 private Drawables mDrawables; 245 246 private CharSequence mError; 247 private boolean mErrorWasChanged; 248 private ErrorPopup mPopup; 249 /** 250 * This flag is set if the TextView tries to display an error before it 251 * is attached to the window (so its position is still unknown). 252 * It causes the error to be shown later, when onAttachedToWindow() 253 * is called. 254 */ 255 private boolean mShowErrorAfterAttach; 256 257 private CharWrapper mCharWrapper = null; 258 259 private boolean mSelectionMoved = false; 260 private boolean mTouchFocusSelected = false; 261 262 private Marquee mMarquee; 263 private boolean mRestartMarquee; 264 265 private int mMarqueeRepeatLimit = 3; 266 267 class InputContentType { 268 int imeOptions = EditorInfo.IME_NULL; 269 String privateImeOptions; 270 CharSequence imeActionLabel; 271 int imeActionId; 272 Bundle extras; 273 OnEditorActionListener onEditorActionListener; 274 boolean enterDown; 275 } 276 InputContentType mInputContentType; 277 278 class InputMethodState { 279 Rect mCursorRectInWindow = new Rect(); 280 RectF mTmpRectF = new RectF(); 281 float[] mTmpOffset = new float[2]; 282 ExtractedTextRequest mExtracting; 283 final ExtractedText mTmpExtracted = new ExtractedText(); 284 int mBatchEditNesting; 285 boolean mCursorChanged; 286 boolean mSelectionModeChanged; 287 boolean mContentChanged; 288 int mChangedStart, mChangedEnd, mChangedDelta; 289 } 290 InputMethodState mInputMethodState; 291 292 /* 293 * Kick-start the font cache for the zygote process (to pay the cost of 294 * initializing freetype for our default font only once). 295 */ 296 static { 297 Paint p = new Paint(); 298 p.setAntiAlias(true); 299 // We don't care about the result, just the side-effect of measuring. 300 p.measureText("H"); 301 } 302 303 /** 304 * Interface definition for a callback to be invoked when an action is 305 * performed on the editor. 306 */ 307 public interface OnEditorActionListener { 308 /** 309 * Called when an action is being performed. 310 * 311 * @param v The view that was clicked. 312 * @param actionId Identifier of the action. This will be either the 313 * identifier you supplied, or {@link EditorInfo#IME_NULL 314 * EditorInfo.IME_NULL} if being called due to the enter key 315 * being pressed. 316 * @param event If triggered by an enter key, this is the event; 317 * otherwise, this is null. 318 * @return Return true if you have consumed the action, else false. 319 */ 320 boolean onEditorAction(TextView v, int actionId, KeyEvent event); 321 } 322 323 public TextView(Context context) { 324 this(context, null); 325 } 326 327 public TextView(Context context, 328 AttributeSet attrs) { 329 this(context, attrs, com.android.internal.R.attr.textViewStyle); 330 } 331 332 @SuppressWarnings("deprecation") 333 public TextView(Context context, 334 AttributeSet attrs, 335 int defStyle) { 336 super(context, attrs, defStyle); 337 mText = ""; 338 339 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 340 mTextPaint.density = getResources().getDisplayMetrics().density; 341 mTextPaint.setCompatibilityScaling( 342 getResources().getCompatibilityInfo().applicationScale); 343 344 // If we get the paint from the skin, we should set it to left, since 345 // the layout always wants it to be left. 346 // mTextPaint.setTextAlign(Paint.Align.LEFT); 347 348 mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 349 mHighlightPaint.setCompatibilityScaling( 350 getResources().getCompatibilityInfo().applicationScale); 351 352 mMovement = getDefaultMovementMethod(); 353 mTransformation = null; 354 355 TypedArray a = 356 context.obtainStyledAttributes( 357 attrs, com.android.internal.R.styleable.TextView, defStyle, 0); 358 359 int textColorHighlight = 0; 360 ColorStateList textColor = null; 361 ColorStateList textColorHint = null; 362 ColorStateList textColorLink = null; 363 int textSize = 15; 364 int typefaceIndex = -1; 365 int styleIndex = -1; 366 367 /* 368 * Look the appearance up without checking first if it exists because 369 * almost every TextView has one and it greatly simplifies the logic 370 * to be able to parse the appearance first and then let specific tags 371 * for this View override it. 372 */ 373 TypedArray appearance = null; 374 int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1); 375 if (ap != -1) { 376 appearance = context.obtainStyledAttributes(ap, 377 com.android.internal.R.styleable. 378 TextAppearance); 379 } 380 if (appearance != null) { 381 int n = appearance.getIndexCount(); 382 for (int i = 0; i < n; i++) { 383 int attr = appearance.getIndex(i); 384 385 switch (attr) { 386 case com.android.internal.R.styleable.TextAppearance_textColorHighlight: 387 textColorHighlight = appearance.getColor(attr, textColorHighlight); 388 break; 389 390 case com.android.internal.R.styleable.TextAppearance_textColor: 391 textColor = appearance.getColorStateList(attr); 392 break; 393 394 case com.android.internal.R.styleable.TextAppearance_textColorHint: 395 textColorHint = appearance.getColorStateList(attr); 396 break; 397 398 case com.android.internal.R.styleable.TextAppearance_textColorLink: 399 textColorLink = appearance.getColorStateList(attr); 400 break; 401 402 case com.android.internal.R.styleable.TextAppearance_textSize: 403 textSize = appearance.getDimensionPixelSize(attr, textSize); 404 break; 405 406 case com.android.internal.R.styleable.TextAppearance_typeface: 407 typefaceIndex = appearance.getInt(attr, -1); 408 break; 409 410 case com.android.internal.R.styleable.TextAppearance_textStyle: 411 styleIndex = appearance.getInt(attr, -1); 412 break; 413 } 414 } 415 416 appearance.recycle(); 417 } 418 419 boolean editable = getDefaultEditable(); 420 CharSequence inputMethod = null; 421 int numeric = 0; 422 CharSequence digits = null; 423 boolean phone = false; 424 boolean autotext = false; 425 int autocap = -1; 426 int buffertype = 0; 427 boolean selectallonfocus = false; 428 Drawable drawableLeft = null, drawableTop = null, drawableRight = null, 429 drawableBottom = null; 430 int drawablePadding = 0; 431 int ellipsize = -1; 432 boolean singleLine = false; 433 int maxlength = -1; 434 CharSequence text = ""; 435 CharSequence hint = null; 436 int shadowcolor = 0; 437 float dx = 0, dy = 0, r = 0; 438 boolean password = false; 439 int inputType = EditorInfo.TYPE_NULL; 440 441 int n = a.getIndexCount(); 442 for (int i = 0; i < n; i++) { 443 int attr = a.getIndex(i); 444 445 switch (attr) { 446 case com.android.internal.R.styleable.TextView_editable: 447 editable = a.getBoolean(attr, editable); 448 break; 449 450 case com.android.internal.R.styleable.TextView_inputMethod: 451 inputMethod = a.getText(attr); 452 break; 453 454 case com.android.internal.R.styleable.TextView_numeric: 455 numeric = a.getInt(attr, numeric); 456 break; 457 458 case com.android.internal.R.styleable.TextView_digits: 459 digits = a.getText(attr); 460 break; 461 462 case com.android.internal.R.styleable.TextView_phoneNumber: 463 phone = a.getBoolean(attr, phone); 464 break; 465 466 case com.android.internal.R.styleable.TextView_autoText: 467 autotext = a.getBoolean(attr, autotext); 468 break; 469 470 case com.android.internal.R.styleable.TextView_capitalize: 471 autocap = a.getInt(attr, autocap); 472 break; 473 474 case com.android.internal.R.styleable.TextView_bufferType: 475 buffertype = a.getInt(attr, buffertype); 476 break; 477 478 case com.android.internal.R.styleable.TextView_selectAllOnFocus: 479 selectallonfocus = a.getBoolean(attr, selectallonfocus); 480 break; 481 482 case com.android.internal.R.styleable.TextView_autoLink: 483 mAutoLinkMask = a.getInt(attr, 0); 484 break; 485 486 case com.android.internal.R.styleable.TextView_linksClickable: 487 mLinksClickable = a.getBoolean(attr, true); 488 break; 489 490 case com.android.internal.R.styleable.TextView_drawableLeft: 491 drawableLeft = a.getDrawable(attr); 492 break; 493 494 case com.android.internal.R.styleable.TextView_drawableTop: 495 drawableTop = a.getDrawable(attr); 496 break; 497 498 case com.android.internal.R.styleable.TextView_drawableRight: 499 drawableRight = a.getDrawable(attr); 500 break; 501 502 case com.android.internal.R.styleable.TextView_drawableBottom: 503 drawableBottom = a.getDrawable(attr); 504 break; 505 506 case com.android.internal.R.styleable.TextView_drawablePadding: 507 drawablePadding = a.getDimensionPixelSize(attr, drawablePadding); 508 break; 509 510 case com.android.internal.R.styleable.TextView_maxLines: 511 setMaxLines(a.getInt(attr, -1)); 512 break; 513 514 case com.android.internal.R.styleable.TextView_maxHeight: 515 setMaxHeight(a.getDimensionPixelSize(attr, -1)); 516 break; 517 518 case com.android.internal.R.styleable.TextView_lines: 519 setLines(a.getInt(attr, -1)); 520 break; 521 522 case com.android.internal.R.styleable.TextView_height: 523 setHeight(a.getDimensionPixelSize(attr, -1)); 524 break; 525 526 case com.android.internal.R.styleable.TextView_minLines: 527 setMinLines(a.getInt(attr, -1)); 528 break; 529 530 case com.android.internal.R.styleable.TextView_minHeight: 531 setMinHeight(a.getDimensionPixelSize(attr, -1)); 532 break; 533 534 case com.android.internal.R.styleable.TextView_maxEms: 535 setMaxEms(a.getInt(attr, -1)); 536 break; 537 538 case com.android.internal.R.styleable.TextView_maxWidth: 539 setMaxWidth(a.getDimensionPixelSize(attr, -1)); 540 break; 541 542 case com.android.internal.R.styleable.TextView_ems: 543 setEms(a.getInt(attr, -1)); 544 break; 545 546 case com.android.internal.R.styleable.TextView_width: 547 setWidth(a.getDimensionPixelSize(attr, -1)); 548 break; 549 550 case com.android.internal.R.styleable.TextView_minEms: 551 setMinEms(a.getInt(attr, -1)); 552 break; 553 554 case com.android.internal.R.styleable.TextView_minWidth: 555 setMinWidth(a.getDimensionPixelSize(attr, -1)); 556 break; 557 558 case com.android.internal.R.styleable.TextView_gravity: 559 setGravity(a.getInt(attr, -1)); 560 break; 561 562 case com.android.internal.R.styleable.TextView_hint: 563 hint = a.getText(attr); 564 break; 565 566 case com.android.internal.R.styleable.TextView_text: 567 text = a.getText(attr); 568 break; 569 570 case com.android.internal.R.styleable.TextView_scrollHorizontally: 571 if (a.getBoolean(attr, false)) { 572 setHorizontallyScrolling(true); 573 } 574 break; 575 576 case com.android.internal.R.styleable.TextView_singleLine: 577 singleLine = a.getBoolean(attr, singleLine); 578 break; 579 580 case com.android.internal.R.styleable.TextView_ellipsize: 581 ellipsize = a.getInt(attr, ellipsize); 582 break; 583 584 case com.android.internal.R.styleable.TextView_marqueeRepeatLimit: 585 setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit)); 586 break; 587 588 case com.android.internal.R.styleable.TextView_includeFontPadding: 589 if (!a.getBoolean(attr, true)) { 590 setIncludeFontPadding(false); 591 } 592 break; 593 594 case com.android.internal.R.styleable.TextView_cursorVisible: 595 if (!a.getBoolean(attr, true)) { 596 setCursorVisible(false); 597 } 598 break; 599 600 case com.android.internal.R.styleable.TextView_maxLength: 601 maxlength = a.getInt(attr, -1); 602 break; 603 604 case com.android.internal.R.styleable.TextView_textScaleX: 605 setTextScaleX(a.getFloat(attr, 1.0f)); 606 break; 607 608 case com.android.internal.R.styleable.TextView_freezesText: 609 mFreezesText = a.getBoolean(attr, false); 610 break; 611 612 case com.android.internal.R.styleable.TextView_shadowColor: 613 shadowcolor = a.getInt(attr, 0); 614 break; 615 616 case com.android.internal.R.styleable.TextView_shadowDx: 617 dx = a.getFloat(attr, 0); 618 break; 619 620 case com.android.internal.R.styleable.TextView_shadowDy: 621 dy = a.getFloat(attr, 0); 622 break; 623 624 case com.android.internal.R.styleable.TextView_shadowRadius: 625 r = a.getFloat(attr, 0); 626 break; 627 628 case com.android.internal.R.styleable.TextView_enabled: 629 setEnabled(a.getBoolean(attr, isEnabled())); 630 break; 631 632 case com.android.internal.R.styleable.TextView_textColorHighlight: 633 textColorHighlight = a.getColor(attr, textColorHighlight); 634 break; 635 636 case com.android.internal.R.styleable.TextView_textColor: 637 textColor = a.getColorStateList(attr); 638 break; 639 640 case com.android.internal.R.styleable.TextView_textColorHint: 641 textColorHint = a.getColorStateList(attr); 642 break; 643 644 case com.android.internal.R.styleable.TextView_textColorLink: 645 textColorLink = a.getColorStateList(attr); 646 break; 647 648 case com.android.internal.R.styleable.TextView_textSize: 649 textSize = a.getDimensionPixelSize(attr, textSize); 650 break; 651 652 case com.android.internal.R.styleable.TextView_typeface: 653 typefaceIndex = a.getInt(attr, typefaceIndex); 654 break; 655 656 case com.android.internal.R.styleable.TextView_textStyle: 657 styleIndex = a.getInt(attr, styleIndex); 658 break; 659 660 case com.android.internal.R.styleable.TextView_password: 661 password = a.getBoolean(attr, password); 662 break; 663 664 case com.android.internal.R.styleable.TextView_lineSpacingExtra: 665 mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd); 666 break; 667 668 case com.android.internal.R.styleable.TextView_lineSpacingMultiplier: 669 mSpacingMult = a.getFloat(attr, mSpacingMult); 670 break; 671 672 case com.android.internal.R.styleable.TextView_inputType: 673 inputType = a.getInt(attr, mInputType); 674 break; 675 676 case com.android.internal.R.styleable.TextView_imeOptions: 677 if (mInputContentType == null) { 678 mInputContentType = new InputContentType(); 679 } 680 mInputContentType.imeOptions = a.getInt(attr, 681 mInputContentType.imeOptions); 682 break; 683 684 case com.android.internal.R.styleable.TextView_imeActionLabel: 685 if (mInputContentType == null) { 686 mInputContentType = new InputContentType(); 687 } 688 mInputContentType.imeActionLabel = a.getText(attr); 689 break; 690 691 case com.android.internal.R.styleable.TextView_imeActionId: 692 if (mInputContentType == null) { 693 mInputContentType = new InputContentType(); 694 } 695 mInputContentType.imeActionId = a.getInt(attr, 696 mInputContentType.imeActionId); 697 break; 698 699 case com.android.internal.R.styleable.TextView_privateImeOptions: 700 setPrivateImeOptions(a.getString(attr)); 701 break; 702 703 case com.android.internal.R.styleable.TextView_editorExtras: 704 try { 705 setInputExtras(a.getResourceId(attr, 0)); 706 } catch (XmlPullParserException e) { 707 Log.w(LOG_TAG, "Failure reading input extras", e); 708 } catch (IOException e) { 709 Log.w(LOG_TAG, "Failure reading input extras", e); 710 } 711 break; 712 } 713 } 714 a.recycle(); 715 716 BufferType bufferType = BufferType.EDITABLE; 717 718 if ((inputType&(EditorInfo.TYPE_MASK_CLASS 719 |EditorInfo.TYPE_MASK_VARIATION)) 720 == (EditorInfo.TYPE_CLASS_TEXT 721 |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { 722 password = true; 723 } 724 725 if (inputMethod != null) { 726 Class<?> c; 727 728 try { 729 c = Class.forName(inputMethod.toString()); 730 } catch (ClassNotFoundException ex) { 731 throw new RuntimeException(ex); 732 } 733 734 try { 735 mInput = (KeyListener) c.newInstance(); 736 } catch (InstantiationException ex) { 737 throw new RuntimeException(ex); 738 } catch (IllegalAccessException ex) { 739 throw new RuntimeException(ex); 740 } 741 try { 742 mInputType = inputType != EditorInfo.TYPE_NULL 743 ? inputType 744 : mInput.getInputType(); 745 } catch (IncompatibleClassChangeError e) { 746 mInputType = EditorInfo.TYPE_CLASS_TEXT; 747 } 748 } else if (digits != null) { 749 mInput = DigitsKeyListener.getInstance(digits.toString()); 750 // If no input type was specified, we will default to generic 751 // text, since we can't tell the IME about the set of digits 752 // that was selected. 753 mInputType = inputType != EditorInfo.TYPE_NULL 754 ? inputType : EditorInfo.TYPE_CLASS_TEXT; 755 } else if (inputType != EditorInfo.TYPE_NULL) { 756 setInputType(inputType, true); 757 singleLine = (inputType&(EditorInfo.TYPE_MASK_CLASS 758 | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) != 759 (EditorInfo.TYPE_CLASS_TEXT 760 | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); 761 } else if (phone) { 762 mInput = DialerKeyListener.getInstance(); 763 mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE; 764 } else if (numeric != 0) { 765 mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0, 766 (numeric & DECIMAL) != 0); 767 inputType = EditorInfo.TYPE_CLASS_NUMBER; 768 if ((numeric & SIGNED) != 0) { 769 inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED; 770 } 771 if ((numeric & DECIMAL) != 0) { 772 inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; 773 } 774 mInputType = inputType; 775 } else if (autotext || autocap != -1) { 776 TextKeyListener.Capitalize cap; 777 778 inputType = EditorInfo.TYPE_CLASS_TEXT; 779 if (!singleLine) { 780 inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; 781 } 782 783 switch (autocap) { 784 case 1: 785 cap = TextKeyListener.Capitalize.SENTENCES; 786 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; 787 break; 788 789 case 2: 790 cap = TextKeyListener.Capitalize.WORDS; 791 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; 792 break; 793 794 case 3: 795 cap = TextKeyListener.Capitalize.CHARACTERS; 796 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS; 797 break; 798 799 default: 800 cap = TextKeyListener.Capitalize.NONE; 801 break; 802 } 803 804 mInput = TextKeyListener.getInstance(autotext, cap); 805 mInputType = inputType; 806 } else if (editable) { 807 mInput = TextKeyListener.getInstance(); 808 mInputType = EditorInfo.TYPE_CLASS_TEXT; 809 } else { 810 mInput = null; 811 812 switch (buffertype) { 813 case 0: 814 bufferType = BufferType.NORMAL; 815 break; 816 case 1: 817 bufferType = BufferType.SPANNABLE; 818 break; 819 case 2: 820 bufferType = BufferType.EDITABLE; 821 break; 822 } 823 } 824 825 if (password && (mInputType&EditorInfo.TYPE_MASK_CLASS) 826 == EditorInfo.TYPE_CLASS_TEXT) { 827 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) 828 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; 829 } 830 831 if (selectallonfocus) { 832 mSelectAllOnFocus = true; 833 834 if (bufferType == BufferType.NORMAL) 835 bufferType = BufferType.SPANNABLE; 836 } 837 838 setCompoundDrawablesWithIntrinsicBounds( 839 drawableLeft, drawableTop, drawableRight, drawableBottom); 840 setCompoundDrawablePadding(drawablePadding); 841 842 if (singleLine) { 843 setSingleLine(); 844 845 if (mInput == null && ellipsize < 0) { 846 ellipsize = 3; // END 847 } 848 } 849 850 switch (ellipsize) { 851 case 1: 852 setEllipsize(TextUtils.TruncateAt.START); 853 break; 854 case 2: 855 setEllipsize(TextUtils.TruncateAt.MIDDLE); 856 break; 857 case 3: 858 setEllipsize(TextUtils.TruncateAt.END); 859 break; 860 case 4: 861 setHorizontalFadingEdgeEnabled(true); 862 setEllipsize(TextUtils.TruncateAt.MARQUEE); 863 break; 864 } 865 866 setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000)); 867 setHintTextColor(textColorHint); 868 setLinkTextColor(textColorLink); 869 if (textColorHighlight != 0) { 870 setHighlightColor(textColorHighlight); 871 } 872 setRawTextSize(textSize); 873 874 if (password) { 875 setTransformationMethod(PasswordTransformationMethod.getInstance()); 876 typefaceIndex = MONOSPACE; 877 } else if ((mInputType&(EditorInfo.TYPE_MASK_CLASS 878 |EditorInfo.TYPE_MASK_VARIATION)) 879 == (EditorInfo.TYPE_CLASS_TEXT 880 |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { 881 typefaceIndex = MONOSPACE; 882 } 883 884 setTypefaceByIndex(typefaceIndex, styleIndex); 885 886 if (shadowcolor != 0) { 887 setShadowLayer(r, dx, dy, shadowcolor); 888 } 889 890 if (maxlength >= 0) { 891 setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) }); 892 } else { 893 setFilters(NO_FILTERS); 894 } 895 896 setText(text, bufferType); 897 if (hint != null) setHint(hint); 898 899 /* 900 * Views are not normally focusable unless specified to be. 901 * However, TextViews that have input or movement methods *are* 902 * focusable by default. 903 */ 904 a = context.obtainStyledAttributes(attrs, 905 com.android.internal.R.styleable.View, 906 defStyle, 0); 907 908 boolean focusable = mMovement != null || mInput != null; 909 boolean clickable = focusable; 910 boolean longClickable = focusable; 911 912 n = a.getIndexCount(); 913 for (int i = 0; i < n; i++) { 914 int attr = a.getIndex(i); 915 916 switch (attr) { 917 case com.android.internal.R.styleable.View_focusable: 918 focusable = a.getBoolean(attr, focusable); 919 break; 920 921 case com.android.internal.R.styleable.View_clickable: 922 clickable = a.getBoolean(attr, clickable); 923 break; 924 925 case com.android.internal.R.styleable.View_longClickable: 926 longClickable = a.getBoolean(attr, longClickable); 927 break; 928 } 929 } 930 a.recycle(); 931 932 setFocusable(focusable); 933 setClickable(clickable); 934 setLongClickable(longClickable); 935 936 prepareCursorControllers(); 937 } 938 939 private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { 940 Typeface tf = null; 941 switch (typefaceIndex) { 942 case SANS: 943 tf = Typeface.SANS_SERIF; 944 break; 945 946 case SERIF: 947 tf = Typeface.SERIF; 948 break; 949 950 case MONOSPACE: 951 tf = Typeface.MONOSPACE; 952 break; 953 } 954 955 setTypeface(tf, styleIndex); 956 } 957 958 /** 959 * Sets the typeface and style in which the text should be displayed, 960 * and turns on the fake bold and italic bits in the Paint if the 961 * Typeface that you provided does not have all the bits in the 962 * style that you specified. 963 * 964 * @attr ref android.R.styleable#TextView_typeface 965 * @attr ref android.R.styleable#TextView_textStyle 966 */ 967 public void setTypeface(Typeface tf, int style) { 968 if (style > 0) { 969 if (tf == null) { 970 tf = Typeface.defaultFromStyle(style); 971 } else { 972 tf = Typeface.create(tf, style); 973 } 974 975 setTypeface(tf); 976 // now compute what (if any) algorithmic styling is needed 977 int typefaceStyle = tf != null ? tf.getStyle() : 0; 978 int need = style & ~typefaceStyle; 979 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); 980 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); 981 } else { 982 mTextPaint.setFakeBoldText(false); 983 mTextPaint.setTextSkewX(0); 984 setTypeface(tf); 985 } 986 } 987 988 /** 989 * Subclasses override this to specify that they have a KeyListener 990 * by default even if not specifically called for in the XML options. 991 */ 992 protected boolean getDefaultEditable() { 993 return false; 994 } 995 996 /** 997 * Subclasses override this to specify a default movement method. 998 */ 999 protected MovementMethod getDefaultMovementMethod() { 1000 return null; 1001 } 1002 1003 /** 1004 * Return the text the TextView is displaying. If setText() was called with 1005 * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast 1006 * the return value from this method to Spannable or Editable, respectively. 1007 * 1008 * Note: The content of the return value should not be modified. If you want 1009 * a modifiable one, you should make your own copy first. 1010 */ 1011 @ViewDebug.CapturedViewProperty 1012 public CharSequence getText() { 1013 return mText; 1014 } 1015 1016 /** 1017 * Returns the length, in characters, of the text managed by this TextView 1018 */ 1019 public int length() { 1020 return mText.length(); 1021 } 1022 1023 /** 1024 * Return the text the TextView is displaying as an Editable object. If 1025 * the text is not editable, null is returned. 1026 * 1027 * @see #getText 1028 */ 1029 public Editable getEditableText() { 1030 return (mText instanceof Editable) ? (Editable)mText : null; 1031 } 1032 1033 /** 1034 * @return the height of one standard line in pixels. Note that markup 1035 * within the text can cause individual lines to be taller or shorter 1036 * than this height, and the layout may contain additional first- 1037 * or last-line padding. 1038 */ 1039 public int getLineHeight() { 1040 return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult 1041 + mSpacingAdd); 1042 } 1043 1044 /** 1045 * @return the Layout that is currently being used to display the text. 1046 * This can be null if the text or width has recently changes. 1047 */ 1048 public final Layout getLayout() { 1049 return mLayout; 1050 } 1051 1052 /** 1053 * @return the current key listener for this TextView. 1054 * This will frequently be null for non-EditText TextViews. 1055 */ 1056 public final KeyListener getKeyListener() { 1057 return mInput; 1058 } 1059 1060 /** 1061 * Sets the key listener to be used with this TextView. This can be null 1062 * to disallow user input. Note that this method has significant and 1063 * subtle interactions with soft keyboards and other input method: 1064 * see {@link KeyListener#getInputType() KeyListener.getContentType()} 1065 * for important details. Calling this method will replace the current 1066 * content type of the text view with the content type returned by the 1067 * key listener. 1068 * <p> 1069 * Be warned that if you want a TextView with a key listener or movement 1070 * method not to be focusable, or if you want a TextView without a 1071 * key listener or movement method to be focusable, you must call 1072 * {@link #setFocusable} again after calling this to get the focusability 1073 * back the way you want it. 1074 * 1075 * @attr ref android.R.styleable#TextView_numeric 1076 * @attr ref android.R.styleable#TextView_digits 1077 * @attr ref android.R.styleable#TextView_phoneNumber 1078 * @attr ref android.R.styleable#TextView_inputMethod 1079 * @attr ref android.R.styleable#TextView_capitalize 1080 * @attr ref android.R.styleable#TextView_autoText 1081 */ 1082 public void setKeyListener(KeyListener input) { 1083 setKeyListenerOnly(input); 1084 fixFocusableAndClickableSettings(); 1085 1086 if (input != null) { 1087 try { 1088 mInputType = mInput.getInputType(); 1089 } catch (IncompatibleClassChangeError e) { 1090 mInputType = EditorInfo.TYPE_CLASS_TEXT; 1091 } 1092 if ((mInputType&EditorInfo.TYPE_MASK_CLASS) 1093 == EditorInfo.TYPE_CLASS_TEXT) { 1094 if (mSingleLine) { 1095 mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; 1096 } else { 1097 mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; 1098 } 1099 } 1100 } else { 1101 mInputType = EditorInfo.TYPE_NULL; 1102 } 1103 1104 InputMethodManager imm = InputMethodManager.peekInstance(); 1105 if (imm != null) imm.restartInput(this); 1106 } 1107 1108 private void setKeyListenerOnly(KeyListener input) { 1109 mInput = input; 1110 if (mInput != null && !(mText instanceof Editable)) 1111 setText(mText); 1112 1113 setFilters((Editable) mText, mFilters); 1114 } 1115 1116 /** 1117 * @return the movement method being used for this TextView. 1118 * This will frequently be null for non-EditText TextViews. 1119 */ 1120 public final MovementMethod getMovementMethod() { 1121 return mMovement; 1122 } 1123 1124 /** 1125 * Sets the movement method (arrow key handler) to be used for 1126 * this TextView. This can be null to disallow using the arrow keys 1127 * to move the cursor or scroll the view. 1128 * <p> 1129 * Be warned that if you want a TextView with a key listener or movement 1130 * method not to be focusable, or if you want a TextView without a 1131 * key listener or movement method to be focusable, you must call 1132 * {@link #setFocusable} again after calling this to get the focusability 1133 * back the way you want it. 1134 */ 1135 public final void setMovementMethod(MovementMethod movement) { 1136 mMovement = movement; 1137 1138 if (mMovement != null && !(mText instanceof Spannable)) 1139 setText(mText); 1140 1141 fixFocusableAndClickableSettings(); 1142 1143 // SelectionModifierCursorController depends on canSelectText, which depends on mMovement 1144 prepareCursorControllers(); 1145 } 1146 1147 private void fixFocusableAndClickableSettings() { 1148 if ((mMovement != null) || mInput != null) { 1149 setFocusable(true); 1150 setClickable(true); 1151 setLongClickable(true); 1152 } else { 1153 setFocusable(false); 1154 setClickable(false); 1155 setLongClickable(false); 1156 } 1157 } 1158 1159 /** 1160 * @return the current transformation method for this TextView. 1161 * This will frequently be null except for single-line and password 1162 * fields. 1163 */ 1164 public final TransformationMethod getTransformationMethod() { 1165 return mTransformation; 1166 } 1167 1168 /** 1169 * Sets the transformation that is applied to the text that this 1170 * TextView is displaying. 1171 * 1172 * @attr ref android.R.styleable#TextView_password 1173 * @attr ref android.R.styleable#TextView_singleLine 1174 */ 1175 public final void setTransformationMethod(TransformationMethod method) { 1176 if (method == mTransformation) { 1177 // Avoid the setText() below if the transformation is 1178 // the same. 1179 return; 1180 } 1181 if (mTransformation != null) { 1182 if (mText instanceof Spannable) { 1183 ((Spannable) mText).removeSpan(mTransformation); 1184 } 1185 } 1186 1187 mTransformation = method; 1188 1189 setText(mText); 1190 } 1191 1192 /** 1193 * Returns the top padding of the view, plus space for the top 1194 * Drawable if any. 1195 */ 1196 public int getCompoundPaddingTop() { 1197 final Drawables dr = mDrawables; 1198 if (dr == null || dr.mDrawableTop == null) { 1199 return mPaddingTop; 1200 } else { 1201 return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop; 1202 } 1203 } 1204 1205 /** 1206 * Returns the bottom padding of the view, plus space for the bottom 1207 * Drawable if any. 1208 */ 1209 public int getCompoundPaddingBottom() { 1210 final Drawables dr = mDrawables; 1211 if (dr == null || dr.mDrawableBottom == null) { 1212 return mPaddingBottom; 1213 } else { 1214 return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom; 1215 } 1216 } 1217 1218 /** 1219 * Returns the left padding of the view, plus space for the left 1220 * Drawable if any. 1221 */ 1222 public int getCompoundPaddingLeft() { 1223 final Drawables dr = mDrawables; 1224 if (dr == null || dr.mDrawableLeft == null) { 1225 return mPaddingLeft; 1226 } else { 1227 return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft; 1228 } 1229 } 1230 1231 /** 1232 * Returns the right padding of the view, plus space for the right 1233 * Drawable if any. 1234 */ 1235 public int getCompoundPaddingRight() { 1236 final Drawables dr = mDrawables; 1237 if (dr == null || dr.mDrawableRight == null) { 1238 return mPaddingRight; 1239 } else { 1240 return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight; 1241 } 1242 } 1243 1244 /** 1245 * Returns the extended top padding of the view, including both the 1246 * top Drawable if any and any extra space to keep more than maxLines 1247 * of text from showing. It is only valid to call this after measuring. 1248 */ 1249 public int getExtendedPaddingTop() { 1250 if (mMaxMode != LINES) { 1251 return getCompoundPaddingTop(); 1252 } 1253 1254 if (mLayout.getLineCount() <= mMaximum) { 1255 return getCompoundPaddingTop(); 1256 } 1257 1258 int top = getCompoundPaddingTop(); 1259 int bottom = getCompoundPaddingBottom(); 1260 int viewht = getHeight() - top - bottom; 1261 int layoutht = mLayout.getLineTop(mMaximum); 1262 1263 if (layoutht >= viewht) { 1264 return top; 1265 } 1266 1267 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 1268 if (gravity == Gravity.TOP) { 1269 return top; 1270 } else if (gravity == Gravity.BOTTOM) { 1271 return top + viewht - layoutht; 1272 } else { // (gravity == Gravity.CENTER_VERTICAL) 1273 return top + (viewht - layoutht) / 2; 1274 } 1275 } 1276 1277 /** 1278 * Returns the extended bottom padding of the view, including both the 1279 * bottom Drawable if any and any extra space to keep more than maxLines 1280 * of text from showing. It is only valid to call this after measuring. 1281 */ 1282 public int getExtendedPaddingBottom() { 1283 if (mMaxMode != LINES) { 1284 return getCompoundPaddingBottom(); 1285 } 1286 1287 if (mLayout.getLineCount() <= mMaximum) { 1288 return getCompoundPaddingBottom(); 1289 } 1290 1291 int top = getCompoundPaddingTop(); 1292 int bottom = getCompoundPaddingBottom(); 1293 int viewht = getHeight() - top - bottom; 1294 int layoutht = mLayout.getLineTop(mMaximum); 1295 1296 if (layoutht >= viewht) { 1297 return bottom; 1298 } 1299 1300 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 1301 if (gravity == Gravity.TOP) { 1302 return bottom + viewht - layoutht; 1303 } else if (gravity == Gravity.BOTTOM) { 1304 return bottom; 1305 } else { // (gravity == Gravity.CENTER_VERTICAL) 1306 return bottom + (viewht - layoutht) / 2; 1307 } 1308 } 1309 1310 /** 1311 * Returns the total left padding of the view, including the left 1312 * Drawable if any. 1313 */ 1314 public int getTotalPaddingLeft() { 1315 return getCompoundPaddingLeft(); 1316 } 1317 1318 /** 1319 * Returns the total right padding of the view, including the right 1320 * Drawable if any. 1321 */ 1322 public int getTotalPaddingRight() { 1323 return getCompoundPaddingRight(); 1324 } 1325 1326 /** 1327 * Returns the total top padding of the view, including the top 1328 * Drawable if any, the extra space to keep more than maxLines 1329 * from showing, and the vertical offset for gravity, if any. 1330 */ 1331 public int getTotalPaddingTop() { 1332 return getExtendedPaddingTop() + getVerticalOffset(true); 1333 } 1334 1335 /** 1336 * Returns the total bottom padding of the view, including the bottom 1337 * Drawable if any, the extra space to keep more than maxLines 1338 * from showing, and the vertical offset for gravity, if any. 1339 */ 1340 public int getTotalPaddingBottom() { 1341 return getExtendedPaddingBottom() + getBottomVerticalOffset(true); 1342 } 1343 1344 /** 1345 * Sets the Drawables (if any) to appear to the left of, above, 1346 * to the right of, and below the text. Use null if you do not 1347 * want a Drawable there. The Drawables must already have had 1348 * {@link Drawable#setBounds} called. 1349 * 1350 * @attr ref android.R.styleable#TextView_drawableLeft 1351 * @attr ref android.R.styleable#TextView_drawableTop 1352 * @attr ref android.R.styleable#TextView_drawableRight 1353 * @attr ref android.R.styleable#TextView_drawableBottom 1354 */ 1355 public void setCompoundDrawables(Drawable left, Drawable top, 1356 Drawable right, Drawable bottom) { 1357 Drawables dr = mDrawables; 1358 1359 final boolean drawables = left != null || top != null 1360 || right != null || bottom != null; 1361 1362 if (!drawables) { 1363 // Clearing drawables... can we free the data structure? 1364 if (dr != null) { 1365 if (dr.mDrawablePadding == 0) { 1366 mDrawables = null; 1367 } else { 1368 // We need to retain the last set padding, so just clear 1369 // out all of the fields in the existing structure. 1370 if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); 1371 dr.mDrawableLeft = null; 1372 if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); 1373 dr.mDrawableTop = null; 1374 if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); 1375 dr.mDrawableRight = null; 1376 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); 1377 dr.mDrawableBottom = null; 1378 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; 1379 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; 1380 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; 1381 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; 1382 } 1383 } 1384 } else { 1385 if (dr == null) { 1386 mDrawables = dr = new Drawables(); 1387 } 1388 1389 if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) { 1390 dr.mDrawableLeft.setCallback(null); 1391 } 1392 dr.mDrawableLeft = left; 1393 1394 if (dr.mDrawableTop != top && dr.mDrawableTop != null) { 1395 dr.mDrawableTop.setCallback(null); 1396 } 1397 dr.mDrawableTop = top; 1398 1399 if (dr.mDrawableRight != right && dr.mDrawableRight != null) { 1400 dr.mDrawableRight.setCallback(null); 1401 } 1402 dr.mDrawableRight = right; 1403 1404 if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { 1405 dr.mDrawableBottom.setCallback(null); 1406 } 1407 dr.mDrawableBottom = bottom; 1408 1409 final Rect compoundRect = dr.mCompoundRect; 1410 int[] state; 1411 1412 state = getDrawableState(); 1413 1414 if (left != null) { 1415 left.setState(state); 1416 left.copyBounds(compoundRect); 1417 left.setCallback(this); 1418 dr.mDrawableSizeLeft = compoundRect.width(); 1419 dr.mDrawableHeightLeft = compoundRect.height(); 1420 } else { 1421 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; 1422 } 1423 1424 if (right != null) { 1425 right.setState(state); 1426 right.copyBounds(compoundRect); 1427 right.setCallback(this); 1428 dr.mDrawableSizeRight = compoundRect.width(); 1429 dr.mDrawableHeightRight = compoundRect.height(); 1430 } else { 1431 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; 1432 } 1433 1434 if (top != null) { 1435 top.setState(state); 1436 top.copyBounds(compoundRect); 1437 top.setCallback(this); 1438 dr.mDrawableSizeTop = compoundRect.height(); 1439 dr.mDrawableWidthTop = compoundRect.width(); 1440 } else { 1441 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; 1442 } 1443 1444 if (bottom != null) { 1445 bottom.setState(state); 1446 bottom.copyBounds(compoundRect); 1447 bottom.setCallback(this); 1448 dr.mDrawableSizeBottom = compoundRect.height(); 1449 dr.mDrawableWidthBottom = compoundRect.width(); 1450 } else { 1451 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; 1452 } 1453 } 1454 1455 invalidate(); 1456 requestLayout(); 1457 } 1458 1459 /** 1460 * Sets the Drawables (if any) to appear to the left of, above, 1461 * to the right of, and below the text. Use 0 if you do not 1462 * want a Drawable there. The Drawables' bounds will be set to 1463 * their intrinsic bounds. 1464 * 1465 * @param left Resource identifier of the left Drawable. 1466 * @param top Resource identifier of the top Drawable. 1467 * @param right Resource identifier of the right Drawable. 1468 * @param bottom Resource identifier of the bottom Drawable. 1469 * 1470 * @attr ref android.R.styleable#TextView_drawableLeft 1471 * @attr ref android.R.styleable#TextView_drawableTop 1472 * @attr ref android.R.styleable#TextView_drawableRight 1473 * @attr ref android.R.styleable#TextView_drawableBottom 1474 */ 1475 public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) { 1476 final Resources resources = getContext().getResources(); 1477 setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null, 1478 top != 0 ? resources.getDrawable(top) : null, 1479 right != 0 ? resources.getDrawable(right) : null, 1480 bottom != 0 ? resources.getDrawable(bottom) : null); 1481 } 1482 1483 /** 1484 * Sets the Drawables (if any) to appear to the left of, above, 1485 * to the right of, and below the text. Use null if you do not 1486 * want a Drawable there. The Drawables' bounds will be set to 1487 * their intrinsic bounds. 1488 * 1489 * @attr ref android.R.styleable#TextView_drawableLeft 1490 * @attr ref android.R.styleable#TextView_drawableTop 1491 * @attr ref android.R.styleable#TextView_drawableRight 1492 * @attr ref android.R.styleable#TextView_drawableBottom 1493 */ 1494 public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, 1495 Drawable right, Drawable bottom) { 1496 1497 if (left != null) { 1498 left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight()); 1499 } 1500 if (right != null) { 1501 right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight()); 1502 } 1503 if (top != null) { 1504 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight()); 1505 } 1506 if (bottom != null) { 1507 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); 1508 } 1509 setCompoundDrawables(left, top, right, bottom); 1510 } 1511 1512 /** 1513 * Returns drawables for the left, top, right, and bottom borders. 1514 */ 1515 public Drawable[] getCompoundDrawables() { 1516 final Drawables dr = mDrawables; 1517 if (dr != null) { 1518 return new Drawable[] { 1519 dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom 1520 }; 1521 } else { 1522 return new Drawable[] { null, null, null, null }; 1523 } 1524 } 1525 1526 /** 1527 * Sets the size of the padding between the compound drawables and 1528 * the text. 1529 * 1530 * @attr ref android.R.styleable#TextView_drawablePadding 1531 */ 1532 public void setCompoundDrawablePadding(int pad) { 1533 Drawables dr = mDrawables; 1534 if (pad == 0) { 1535 if (dr != null) { 1536 dr.mDrawablePadding = pad; 1537 } 1538 } else { 1539 if (dr == null) { 1540 mDrawables = dr = new Drawables(); 1541 } 1542 dr.mDrawablePadding = pad; 1543 } 1544 1545 invalidate(); 1546 requestLayout(); 1547 } 1548 1549 /** 1550 * Returns the padding between the compound drawables and the text. 1551 */ 1552 public int getCompoundDrawablePadding() { 1553 final Drawables dr = mDrawables; 1554 return dr != null ? dr.mDrawablePadding : 0; 1555 } 1556 1557 @Override 1558 public void setPadding(int left, int top, int right, int bottom) { 1559 if (left != mPaddingLeft || 1560 right != mPaddingRight || 1561 top != mPaddingTop || 1562 bottom != mPaddingBottom) { 1563 nullLayouts(); 1564 } 1565 1566 // the super call will requestLayout() 1567 super.setPadding(left, top, right, bottom); 1568 invalidate(); 1569 } 1570 1571 /** 1572 * Gets the autolink mask of the text. See {@link 1573 * android.text.util.Linkify#ALL Linkify.ALL} and peers for 1574 * possible values. 1575 * 1576 * @attr ref android.R.styleable#TextView_autoLink 1577 */ 1578 public final int getAutoLinkMask() { 1579 return mAutoLinkMask; 1580 } 1581 1582 /** 1583 * Sets the text color, size, style, hint color, and highlight color 1584 * from the specified TextAppearance resource. 1585 */ 1586 public void setTextAppearance(Context context, int resid) { 1587 TypedArray appearance = 1588 context.obtainStyledAttributes(resid, 1589 com.android.internal.R.styleable.TextAppearance); 1590 1591 int color; 1592 ColorStateList colors; 1593 int ts; 1594 1595 color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0); 1596 if (color != 0) { 1597 setHighlightColor(color); 1598 } 1599 1600 colors = appearance.getColorStateList(com.android.internal.R.styleable. 1601 TextAppearance_textColor); 1602 if (colors != null) { 1603 setTextColor(colors); 1604 } 1605 1606 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable. 1607 TextAppearance_textSize, 0); 1608 if (ts != 0) { 1609 setRawTextSize(ts); 1610 } 1611 1612 colors = appearance.getColorStateList(com.android.internal.R.styleable. 1613 TextAppearance_textColorHint); 1614 if (colors != null) { 1615 setHintTextColor(colors); 1616 } 1617 1618 colors = appearance.getColorStateList(com.android.internal.R.styleable. 1619 TextAppearance_textColorLink); 1620 if (colors != null) { 1621 setLinkTextColor(colors); 1622 } 1623 1624 int typefaceIndex, styleIndex; 1625 1626 typefaceIndex = appearance.getInt(com.android.internal.R.styleable. 1627 TextAppearance_typeface, -1); 1628 styleIndex = appearance.getInt(com.android.internal.R.styleable. 1629 TextAppearance_textStyle, -1); 1630 1631 setTypefaceByIndex(typefaceIndex, styleIndex); 1632 appearance.recycle(); 1633 } 1634 1635 /** 1636 * @return the size (in pixels) of the default text size in this TextView. 1637 */ 1638 public float getTextSize() { 1639 return mTextPaint.getTextSize(); 1640 } 1641 1642 /** 1643 * Set the default text size to the given value, interpreted as "scaled 1644 * pixel" units. This size is adjusted based on the current density and 1645 * user font size preference. 1646 * 1647 * @param size The scaled pixel size. 1648 * 1649 * @attr ref android.R.styleable#TextView_textSize 1650 */ 1651 @android.view.RemotableViewMethod 1652 public void setTextSize(float size) { 1653 setTextSize(TypedValue.COMPLEX_UNIT_SP, size); 1654 } 1655 1656 /** 1657 * Set the default text size to a given unit and value. See {@link 1658 * TypedValue} for the possible dimension units. 1659 * 1660 * @param unit The desired dimension unit. 1661 * @param size The desired size in the given units. 1662 * 1663 * @attr ref android.R.styleable#TextView_textSize 1664 */ 1665 public void setTextSize(int unit, float size) { 1666 Context c = getContext(); 1667 Resources r; 1668 1669 if (c == null) 1670 r = Resources.getSystem(); 1671 else 1672 r = c.getResources(); 1673 1674 setRawTextSize(TypedValue.applyDimension( 1675 unit, size, r.getDisplayMetrics())); 1676 } 1677 1678 private void setRawTextSize(float size) { 1679 if (size != mTextPaint.getTextSize()) { 1680 mTextPaint.setTextSize(size); 1681 1682 if (mLayout != null) { 1683 nullLayouts(); 1684 requestLayout(); 1685 invalidate(); 1686 } 1687 } 1688 } 1689 1690 /** 1691 * @return the extent by which text is currently being stretched 1692 * horizontally. This will usually be 1. 1693 */ 1694 public float getTextScaleX() { 1695 return mTextPaint.getTextScaleX(); 1696 } 1697 1698 /** 1699 * Sets the extent by which text should be stretched horizontally. 1700 * 1701 * @attr ref android.R.styleable#TextView_textScaleX 1702 */ 1703 @android.view.RemotableViewMethod 1704 public void setTextScaleX(float size) { 1705 if (size != mTextPaint.getTextScaleX()) { 1706 mUserSetTextScaleX = true; 1707 mTextPaint.setTextScaleX(size); 1708 1709 if (mLayout != null) { 1710 nullLayouts(); 1711 requestLayout(); 1712 invalidate(); 1713 } 1714 } 1715 } 1716 1717 /** 1718 * Sets the typeface and style in which the text should be displayed. 1719 * Note that not all Typeface families actually have bold and italic 1720 * variants, so you may need to use 1721 * {@link #setTypeface(Typeface, int)} to get the appearance 1722 * that you actually want. 1723 * 1724 * @attr ref android.R.styleable#TextView_typeface 1725 * @attr ref android.R.styleable#TextView_textStyle 1726 */ 1727 public void setTypeface(Typeface tf) { 1728 if (mTextPaint.getTypeface() != tf) { 1729 mTextPaint.setTypeface(tf); 1730 1731 if (mLayout != null) { 1732 nullLayouts(); 1733 requestLayout(); 1734 invalidate(); 1735 } 1736 } 1737 } 1738 1739 /** 1740 * @return the current typeface and style in which the text is being 1741 * displayed. 1742 */ 1743 public Typeface getTypeface() { 1744 return mTextPaint.getTypeface(); 1745 } 1746 1747 /** 1748 * Sets the text color for all the states (normal, selected, 1749 * focused) to be this color. 1750 * 1751 * @attr ref android.R.styleable#TextView_textColor 1752 */ 1753 @android.view.RemotableViewMethod 1754 public void setTextColor(int color) { 1755 mTextColor = ColorStateList.valueOf(color); 1756 updateTextColors(); 1757 } 1758 1759 /** 1760 * Sets the text color. 1761 * 1762 * @attr ref android.R.styleable#TextView_textColor 1763 */ 1764 public void setTextColor(ColorStateList colors) { 1765 if (colors == null) { 1766 throw new NullPointerException(); 1767 } 1768 1769 mTextColor = colors; 1770 updateTextColors(); 1771 } 1772 1773 /** 1774 * Return the set of text colors. 1775 * 1776 * @return Returns the set of text colors. 1777 */ 1778 public final ColorStateList getTextColors() { 1779 return mTextColor; 1780 } 1781 1782 /** 1783 * <p>Return the current color selected for normal text.</p> 1784 * 1785 * @return Returns the current text color. 1786 */ 1787 public final int getCurrentTextColor() { 1788 return mCurTextColor; 1789 } 1790 1791 /** 1792 * Sets the color used to display the selection highlight. 1793 * 1794 * @attr ref android.R.styleable#TextView_textColorHighlight 1795 */ 1796 @android.view.RemotableViewMethod 1797 public void setHighlightColor(int color) { 1798 if (mHighlightColor != color) { 1799 mHighlightColor = color; 1800 invalidate(); 1801 } 1802 } 1803 1804 /** 1805 * Gives the text a shadow of the specified radius and color, the specified 1806 * distance from its normal position. 1807 * 1808 * @attr ref android.R.styleable#TextView_shadowColor 1809 * @attr ref android.R.styleable#TextView_shadowDx 1810 * @attr ref android.R.styleable#TextView_shadowDy 1811 * @attr ref android.R.styleable#TextView_shadowRadius 1812 */ 1813 public void setShadowLayer(float radius, float dx, float dy, int color) { 1814 mTextPaint.setShadowLayer(radius, dx, dy, color); 1815 1816 mShadowRadius = radius; 1817 mShadowDx = dx; 1818 mShadowDy = dy; 1819 1820 invalidate(); 1821 } 1822 1823 /** 1824 * @return the base paint used for the text. Please use this only to 1825 * consult the Paint's properties and not to change them. 1826 */ 1827 public TextPaint getPaint() { 1828 return mTextPaint; 1829 } 1830 1831 /** 1832 * Sets the autolink mask of the text. See {@link 1833 * android.text.util.Linkify#ALL Linkify.ALL} and peers for 1834 * possible values. 1835 * 1836 * @attr ref android.R.styleable#TextView_autoLink 1837 */ 1838 @android.view.RemotableViewMethod 1839 public final void setAutoLinkMask(int mask) { 1840 mAutoLinkMask = mask; 1841 } 1842 1843 /** 1844 * Sets whether the movement method will automatically be set to 1845 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been 1846 * set to nonzero and links are detected in {@link #setText}. 1847 * The default is true. 1848 * 1849 * @attr ref android.R.styleable#TextView_linksClickable 1850 */ 1851 @android.view.RemotableViewMethod 1852 public final void setLinksClickable(boolean whether) { 1853 mLinksClickable = whether; 1854 } 1855 1856 /** 1857 * Returns whether the movement method will automatically be set to 1858 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been 1859 * set to nonzero and links are detected in {@link #setText}. 1860 * The default is true. 1861 * 1862 * @attr ref android.R.styleable#TextView_linksClickable 1863 */ 1864 public final boolean getLinksClickable() { 1865 return mLinksClickable; 1866 } 1867 1868 /** 1869 * Returns the list of URLSpans attached to the text 1870 * (by {@link Linkify} or otherwise) if any. You can call 1871 * {@link URLSpan#getURL} on them to find where they link to 1872 * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd} 1873 * to find the region of the text they are attached to. 1874 */ 1875 public URLSpan[] getUrls() { 1876 if (mText instanceof Spanned) { 1877 return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class); 1878 } else { 1879 return new URLSpan[0]; 1880 } 1881 } 1882 1883 /** 1884 * Sets the color of the hint text. 1885 * 1886 * @attr ref android.R.styleable#TextView_textColorHint 1887 */ 1888 @android.view.RemotableViewMethod 1889 public final void setHintTextColor(int color) { 1890 mHintTextColor = ColorStateList.valueOf(color); 1891 updateTextColors(); 1892 } 1893 1894 /** 1895 * Sets the color of the hint text. 1896 * 1897 * @attr ref android.R.styleable#TextView_textColorHint 1898 */ 1899 public final void setHintTextColor(ColorStateList colors) { 1900 mHintTextColor = colors; 1901 updateTextColors(); 1902 } 1903 1904 /** 1905 * <p>Return the color used to paint the hint text.</p> 1906 * 1907 * @return Returns the list of hint text colors. 1908 */ 1909 public final ColorStateList getHintTextColors() { 1910 return mHintTextColor; 1911 } 1912 1913 /** 1914 * <p>Return the current color selected to paint the hint text.</p> 1915 * 1916 * @return Returns the current hint text color. 1917 */ 1918 public final int getCurrentHintTextColor() { 1919 return mHintTextColor != null ? mCurHintTextColor : mCurTextColor; 1920 } 1921 1922 /** 1923 * Sets the color of links in the text. 1924 * 1925 * @attr ref android.R.styleable#TextView_textColorLink 1926 */ 1927 @android.view.RemotableViewMethod 1928 public final void setLinkTextColor(int color) { 1929 mLinkTextColor = ColorStateList.valueOf(color); 1930 updateTextColors(); 1931 } 1932 1933 /** 1934 * Sets the color of links in the text. 1935 * 1936 * @attr ref android.R.styleable#TextView_textColorLink 1937 */ 1938 public final void setLinkTextColor(ColorStateList colors) { 1939 mLinkTextColor = colors; 1940 updateTextColors(); 1941 } 1942 1943 /** 1944 * <p>Returns the color used to paint links in the text.</p> 1945 * 1946 * @return Returns the list of link text colors. 1947 */ 1948 public final ColorStateList getLinkTextColors() { 1949 return mLinkTextColor; 1950 } 1951 1952 /** 1953 * Sets the horizontal alignment of the text and the 1954 * vertical gravity that will be used when there is extra space 1955 * in the TextView beyond what is required for the text itself. 1956 * 1957 * @see android.view.Gravity 1958 * @attr ref android.R.styleable#TextView_gravity 1959 */ 1960 public void setGravity(int gravity) { 1961 if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { 1962 gravity |= Gravity.LEFT; 1963 } 1964 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { 1965 gravity |= Gravity.TOP; 1966 } 1967 1968 boolean newLayout = false; 1969 1970 if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != 1971 (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) { 1972 newLayout = true; 1973 } 1974 1975 if (gravity != mGravity) { 1976 invalidate(); 1977 } 1978 1979 mGravity = gravity; 1980 1981 if (mLayout != null && newLayout) { 1982 // XXX this is heavy-handed because no actual content changes. 1983 int want = mLayout.getWidth(); 1984 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth(); 1985 1986 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING, 1987 mRight - mLeft - getCompoundPaddingLeft() - 1988 getCompoundPaddingRight(), true); 1989 } 1990 } 1991 1992 /** 1993 * Returns the horizontal and vertical alignment of this TextView. 1994 * 1995 * @see android.view.Gravity 1996 * @attr ref android.R.styleable#TextView_gravity 1997 */ 1998 public int getGravity() { 1999 return mGravity; 2000 } 2001 2002 /** 2003 * @return the flags on the Paint being used to display the text. 2004 * @see Paint#getFlags 2005 */ 2006 public int getPaintFlags() { 2007 return mTextPaint.getFlags(); 2008 } 2009 2010 /** 2011 * Sets flags on the Paint being used to display the text and 2012 * reflows the text if they are different from the old flags. 2013 * @see Paint#setFlags 2014 */ 2015 @android.view.RemotableViewMethod 2016 public void setPaintFlags(int flags) { 2017 if (mTextPaint.getFlags() != flags) { 2018 mTextPaint.setFlags(flags); 2019 2020 if (mLayout != null) { 2021 nullLayouts(); 2022 requestLayout(); 2023 invalidate(); 2024 } 2025 } 2026 } 2027 2028 /** 2029 * Sets whether the text should be allowed to be wider than the 2030 * View is. If false, it will be wrapped to the width of the View. 2031 * 2032 * @attr ref android.R.styleable#TextView_scrollHorizontally 2033 */ 2034 public void setHorizontallyScrolling(boolean whether) { 2035 mHorizontallyScrolling = whether; 2036 2037 if (mLayout != null) { 2038 nullLayouts(); 2039 requestLayout(); 2040 invalidate(); 2041 } 2042 } 2043 2044 /** 2045 * Makes the TextView at least this many lines tall 2046 * 2047 * @attr ref android.R.styleable#TextView_minLines 2048 */ 2049 @android.view.RemotableViewMethod 2050 public void setMinLines(int minlines) { 2051 mMinimum = minlines; 2052 mMinMode = LINES; 2053 2054 requestLayout(); 2055 invalidate(); 2056 } 2057 2058 /** 2059 * Makes the TextView at least this many pixels tall 2060 * 2061 * @attr ref android.R.styleable#TextView_minHeight 2062 */ 2063 @android.view.RemotableViewMethod 2064 public void setMinHeight(int minHeight) { 2065 mMinimum = minHeight; 2066 mMinMode = PIXELS; 2067 2068 requestLayout(); 2069 invalidate(); 2070 } 2071 2072 /** 2073 * Makes the TextView at most this many lines tall 2074 * 2075 * @attr ref android.R.styleable#TextView_maxLines 2076 */ 2077 @android.view.RemotableViewMethod 2078 public void setMaxLines(int maxlines) { 2079 mMaximum = maxlines; 2080 mMaxMode = LINES; 2081 2082 requestLayout(); 2083 invalidate(); 2084 } 2085 2086 /** 2087 * Makes the TextView at most this many pixels tall 2088 * 2089 * @attr ref android.R.styleable#TextView_maxHeight 2090 */ 2091 @android.view.RemotableViewMethod 2092 public void setMaxHeight(int maxHeight) { 2093 mMaximum = maxHeight; 2094 mMaxMode = PIXELS; 2095 2096 requestLayout(); 2097 invalidate(); 2098 } 2099 2100 /** 2101 * Makes the TextView exactly this many lines tall 2102 * 2103 * @attr ref android.R.styleable#TextView_lines 2104 */ 2105 @android.view.RemotableViewMethod 2106 public void setLines(int lines) { 2107 mMaximum = mMinimum = lines; 2108 mMaxMode = mMinMode = LINES; 2109 2110 requestLayout(); 2111 invalidate(); 2112 } 2113 2114 /** 2115 * Makes the TextView exactly this many pixels tall. 2116 * You could do the same thing by specifying this number in the 2117 * LayoutParams. 2118 * 2119 * @attr ref android.R.styleable#TextView_height 2120 */ 2121 @android.view.RemotableViewMethod 2122 public void setHeight(int pixels) { 2123 mMaximum = mMinimum = pixels; 2124 mMaxMode = mMinMode = PIXELS; 2125 2126 requestLayout(); 2127 invalidate(); 2128 } 2129 2130 /** 2131 * Makes the TextView at least this many ems wide 2132 * 2133 * @attr ref android.R.styleable#TextView_minEms 2134 */ 2135 @android.view.RemotableViewMethod 2136 public void setMinEms(int minems) { 2137 mMinWidth = minems; 2138 mMinWidthMode = EMS; 2139 2140 requestLayout(); 2141 invalidate(); 2142 } 2143 2144 /** 2145 * Makes the TextView at least this many pixels wide 2146 * 2147 * @attr ref android.R.styleable#TextView_minWidth 2148 */ 2149 @android.view.RemotableViewMethod 2150 public void setMinWidth(int minpixels) { 2151 mMinWidth = minpixels; 2152 mMinWidthMode = PIXELS; 2153 2154 requestLayout(); 2155 invalidate(); 2156 } 2157 2158 /** 2159 * Makes the TextView at most this many ems wide 2160 * 2161 * @attr ref android.R.styleable#TextView_maxEms 2162 */ 2163 @android.view.RemotableViewMethod 2164 public void setMaxEms(int maxems) { 2165 mMaxWidth = maxems; 2166 mMaxWidthMode = EMS; 2167 2168 requestLayout(); 2169 invalidate(); 2170 } 2171 2172 /** 2173 * Makes the TextView at most this many pixels wide 2174 * 2175 * @attr ref android.R.styleable#TextView_maxWidth 2176 */ 2177 @android.view.RemotableViewMethod 2178 public void setMaxWidth(int maxpixels) { 2179 mMaxWidth = maxpixels; 2180 mMaxWidthMode = PIXELS; 2181 2182 requestLayout(); 2183 invalidate(); 2184 } 2185 2186 /** 2187 * Makes the TextView exactly this many ems wide 2188 * 2189 * @attr ref android.R.styleable#TextView_ems 2190 */ 2191 @android.view.RemotableViewMethod 2192 public void setEms(int ems) { 2193 mMaxWidth = mMinWidth = ems; 2194 mMaxWidthMode = mMinWidthMode = EMS; 2195 2196 requestLayout(); 2197 invalidate(); 2198 } 2199 2200 /** 2201 * Makes the TextView exactly this many pixels wide. 2202 * You could do the same thing by specifying this number in the 2203 * LayoutParams. 2204 * 2205 * @attr ref android.R.styleable#TextView_width 2206 */ 2207 @android.view.RemotableViewMethod 2208 public void setWidth(int pixels) { 2209 mMaxWidth = mMinWidth = pixels; 2210 mMaxWidthMode = mMinWidthMode = PIXELS; 2211 2212 requestLayout(); 2213 invalidate(); 2214 } 2215 2216 2217 /** 2218 * Sets line spacing for this TextView. Each line will have its height 2219 * multiplied by <code>mult</code> and have <code>add</code> added to it. 2220 * 2221 * @attr ref android.R.styleable#TextView_lineSpacingExtra 2222 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier 2223 */ 2224 public void setLineSpacing(float add, float mult) { 2225 mSpacingMult = mult; 2226 mSpacingAdd = add; 2227 2228 if (mLayout != null) { 2229 nullLayouts(); 2230 requestLayout(); 2231 invalidate(); 2232 } 2233 } 2234 2235 /** 2236 * Convenience method: Append the specified text to the TextView's 2237 * display buffer, upgrading it to BufferType.EDITABLE if it was 2238 * not already editable. 2239 */ 2240 public final void append(CharSequence text) { 2241 append(text, 0, text.length()); 2242 } 2243 2244 /** 2245 * Convenience method: Append the specified text slice to the TextView's 2246 * display buffer, upgrading it to BufferType.EDITABLE if it was 2247 * not already editable. 2248 */ 2249 public void append(CharSequence text, int start, int end) { 2250 if (!(mText instanceof Editable)) { 2251 setText(mText, BufferType.EDITABLE); 2252 } 2253 2254 ((Editable) mText).append(text, start, end); 2255 } 2256 2257 private void updateTextColors() { 2258 boolean inval = false; 2259 int color = mTextColor.getColorForState(getDrawableState(), 0); 2260 if (color != mCurTextColor) { 2261 mCurTextColor = color; 2262 inval = true; 2263 } 2264 if (mLinkTextColor != null) { 2265 color = mLinkTextColor.getColorForState(getDrawableState(), 0); 2266 if (color != mTextPaint.linkColor) { 2267 mTextPaint.linkColor = color; 2268 inval = true; 2269 } 2270 } 2271 if (mHintTextColor != null) { 2272 color = mHintTextColor.getColorForState(getDrawableState(), 0); 2273 if (color != mCurHintTextColor && mText.length() == 0) { 2274 mCurHintTextColor = color; 2275 inval = true; 2276 } 2277 } 2278 if (inval) { 2279 invalidate(); 2280 } 2281 } 2282 2283 @Override 2284 protected void drawableStateChanged() { 2285 super.drawableStateChanged(); 2286 if (mTextColor != null && mTextColor.isStateful() 2287 || (mHintTextColor != null && mHintTextColor.isStateful()) 2288 || (mLinkTextColor != null && mLinkTextColor.isStateful())) { 2289 updateTextColors(); 2290 } 2291 2292 final Drawables dr = mDrawables; 2293 if (dr != null) { 2294 int[] state = getDrawableState(); 2295 if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) { 2296 dr.mDrawableTop.setState(state); 2297 } 2298 if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) { 2299 dr.mDrawableBottom.setState(state); 2300 } 2301 if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) { 2302 dr.mDrawableLeft.setState(state); 2303 } 2304 if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) { 2305 dr.mDrawableRight.setState(state); 2306 } 2307 } 2308 } 2309 2310 /** 2311 * User interface state that is stored by TextView for implementing 2312 * {@link View#onSaveInstanceState}. 2313 */ 2314 public static class SavedState extends BaseSavedState { 2315 int selStart; 2316 int selEnd; 2317 CharSequence text; 2318 boolean frozenWithFocus; 2319 CharSequence error; 2320 2321 SavedState(Parcelable superState) { 2322 super(superState); 2323 } 2324 2325 @Override 2326 public void writeToParcel(Parcel out, int flags) { 2327 super.writeToParcel(out, flags); 2328 out.writeInt(selStart); 2329 out.writeInt(selEnd); 2330 out.writeInt(frozenWithFocus ? 1 : 0); 2331 TextUtils.writeToParcel(text, out, flags); 2332 2333 if (error == null) { 2334 out.writeInt(0); 2335 } else { 2336 out.writeInt(1); 2337 TextUtils.writeToParcel(error, out, flags); 2338 } 2339 } 2340 2341 @Override 2342 public String toString() { 2343 String str = "TextView.SavedState{" 2344 + Integer.toHexString(System.identityHashCode(this)) 2345 + " start=" + selStart + " end=" + selEnd; 2346 if (text != null) { 2347 str += " text=" + text; 2348 } 2349 return str + "}"; 2350 } 2351 2352 @SuppressWarnings("hiding") 2353 public static final Parcelable.Creator<SavedState> CREATOR 2354 = new Parcelable.Creator<SavedState>() { 2355 public SavedState createFromParcel(Parcel in) { 2356 return new SavedState(in); 2357 } 2358 2359 public SavedState[] newArray(int size) { 2360 return new SavedState[size]; 2361 } 2362 }; 2363 2364 private SavedState(Parcel in) { 2365 super(in); 2366 selStart = in.readInt(); 2367 selEnd = in.readInt(); 2368 frozenWithFocus = (in.readInt() != 0); 2369 text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 2370 2371 if (in.readInt() != 0) { 2372 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 2373 } 2374 } 2375 } 2376 2377 @Override 2378 public Parcelable onSaveInstanceState() { 2379 Parcelable superState = super.onSaveInstanceState(); 2380 2381 // Save state if we are forced to 2382 boolean save = mFreezesText; 2383 int start = 0; 2384 int end = 0; 2385 2386 if (mText != null) { 2387 start = getSelectionStart(); 2388 end = getSelectionEnd(); 2389 if (start >= 0 || end >= 0) { 2390 // Or save state if there is a selection 2391 save = true; 2392 } 2393 } 2394 2395 if (save) { 2396 SavedState ss = new SavedState(superState); 2397 // XXX Should also save the current scroll position! 2398 ss.selStart = start; 2399 ss.selEnd = end; 2400 2401 if (mText instanceof Spanned) { 2402 /* 2403 * Calling setText() strips off any ChangeWatchers; 2404 * strip them now to avoid leaking references. 2405 * But do it to a copy so that if there are any 2406 * further changes to the text of this view, it 2407 * won't get into an inconsistent state. 2408 */ 2409 2410 Spannable sp = new SpannableString(mText); 2411 2412 for (ChangeWatcher cw : 2413 sp.getSpans(0, sp.length(), ChangeWatcher.class)) { 2414 sp.removeSpan(cw); 2415 } 2416 2417 ss.text = sp; 2418 } else { 2419 ss.text = mText.toString(); 2420 } 2421 2422 if (isFocused() && start >= 0 && end >= 0) { 2423 ss.frozenWithFocus = true; 2424 } 2425 2426 ss.error = mError; 2427 2428 return ss; 2429 } 2430 2431 return superState; 2432 } 2433 2434 @Override 2435 public void onRestoreInstanceState(Parcelable state) { 2436 if (!(state instanceof SavedState)) { 2437 super.onRestoreInstanceState(state); 2438 return; 2439 } 2440 2441 SavedState ss = (SavedState)state; 2442 super.onRestoreInstanceState(ss.getSuperState()); 2443 2444 // XXX restore buffer type too, as well as lots of other stuff 2445 if (ss.text != null) { 2446 setText(ss.text); 2447 } 2448 2449 if (ss.selStart >= 0 && ss.selEnd >= 0) { 2450 if (mText instanceof Spannable) { 2451 int len = mText.length(); 2452 2453 if (ss.selStart > len || ss.selEnd > len) { 2454 String restored = ""; 2455 2456 if (ss.text != null) { 2457 restored = "(restored) "; 2458 } 2459 2460 Log.e(LOG_TAG, "Saved cursor position " + ss.selStart + 2461 "/" + ss.selEnd + " out of range for " + restored + 2462 "text " + mText); 2463 } else { 2464 Selection.setSelection((Spannable) mText, ss.selStart, 2465 ss.selEnd); 2466 2467 if (ss.frozenWithFocus) { 2468 mFrozenWithFocus = true; 2469 } 2470 } 2471 } 2472 } 2473 2474 if (ss.error != null) { 2475 final CharSequence error = ss.error; 2476 // Display the error later, after the first layout pass 2477 post(new Runnable() { 2478 public void run() { 2479 setError(error); 2480 } 2481 }); 2482 } 2483 } 2484 2485 /** 2486 * Control whether this text view saves its entire text contents when 2487 * freezing to an icicle, in addition to dynamic state such as cursor 2488 * position. By default this is false, not saving the text. Set to true 2489 * if the text in the text view is not being saved somewhere else in 2490 * persistent storage (such as in a content provider) so that if the 2491 * view is later thawed the user will not lose their data. 2492 * 2493 * @param freezesText Controls whether a frozen icicle should include the 2494 * entire text data: true to include it, false to not. 2495 * 2496 * @attr ref android.R.styleable#TextView_freezesText 2497 */ 2498 @android.view.RemotableViewMethod 2499 public void setFreezesText(boolean freezesText) { 2500 mFreezesText = freezesText; 2501 } 2502 2503 /** 2504 * Return whether this text view is including its entire text contents 2505 * in frozen icicles. 2506 * 2507 * @return Returns true if text is included, false if it isn't. 2508 * 2509 * @see #setFreezesText 2510 */ 2511 public boolean getFreezesText() { 2512 return mFreezesText; 2513 } 2514 2515 /////////////////////////////////////////////////////////////////////////// 2516 2517 /** 2518 * Sets the Factory used to create new Editables. 2519 */ 2520 public final void setEditableFactory(Editable.Factory factory) { 2521 mEditableFactory = factory; 2522 setText(mText); 2523 } 2524 2525 /** 2526 * Sets the Factory used to create new Spannables. 2527 */ 2528 public final void setSpannableFactory(Spannable.Factory factory) { 2529 mSpannableFactory = factory; 2530 setText(mText); 2531 } 2532 2533 /** 2534 * Sets the string value of the TextView. TextView <em>does not</em> accept 2535 * HTML-like formatting, which you can do with text strings in XML resource files. 2536 * To style your strings, attach android.text.style.* objects to a 2537 * {@link android.text.SpannableString SpannableString}, or see the 2538 * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources"> 2539 * Available Resource Types</a> documentation for an example of setting 2540 * formatted text in the XML resource file. 2541 * 2542 * @attr ref android.R.styleable#TextView_text 2543 */ 2544 @android.view.RemotableViewMethod 2545 public final void setText(CharSequence text) { 2546 setText(text, mBufferType); 2547 } 2548 2549 /** 2550 * Like {@link #setText(CharSequence)}, 2551 * except that the cursor position (if any) is retained in the new text. 2552 * 2553 * @param text The new text to place in the text view. 2554 * 2555 * @see #setText(CharSequence) 2556 */ 2557 @android.view.RemotableViewMethod 2558 public final void setTextKeepState(CharSequence text) { 2559 setTextKeepState(text, mBufferType); 2560 } 2561 2562 /** 2563 * Sets the text that this TextView is to display (see 2564 * {@link #setText(CharSequence)}) and also sets whether it is stored 2565 * in a styleable/spannable buffer and whether it is editable. 2566 * 2567 * @attr ref android.R.styleable#TextView_text 2568 * @attr ref android.R.styleable#TextView_bufferType 2569 */ 2570 public void setText(CharSequence text, BufferType type) { 2571 setText(text, type, true, 0); 2572 2573 if (mCharWrapper != null) { 2574 mCharWrapper.mChars = null; 2575 } 2576 } 2577 2578 private void setText(CharSequence text, BufferType type, 2579 boolean notifyBefore, int oldlen) { 2580 if (text == null) { 2581 text = ""; 2582 } 2583 2584 if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f); 2585 2586 if (text instanceof Spanned && 2587 ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) { 2588 setHorizontalFadingEdgeEnabled(true); 2589 setEllipsize(TextUtils.TruncateAt.MARQUEE); 2590 } 2591 2592 int n = mFilters.length; 2593 for (int i = 0; i < n; i++) { 2594 CharSequence out = mFilters[i].filter(text, 0, text.length(), 2595 EMPTY_SPANNED, 0, 0); 2596 if (out != null) { 2597 text = out; 2598 } 2599 } 2600 2601 if (notifyBefore) { 2602 if (mText != null) { 2603 oldlen = mText.length(); 2604 sendBeforeTextChanged(mText, 0, oldlen, text.length()); 2605 } else { 2606 sendBeforeTextChanged("", 0, 0, text.length()); 2607 } 2608 } 2609 2610 boolean needEditableForNotification = false; 2611 2612 if (mListeners != null && mListeners.size() != 0) { 2613 needEditableForNotification = true; 2614 } 2615 2616 if (type == BufferType.EDITABLE || mInput != null || 2617 needEditableForNotification) { 2618 Editable t = mEditableFactory.newEditable(text); 2619 text = t; 2620 setFilters(t, mFilters); 2621 InputMethodManager imm = InputMethodManager.peekInstance(); 2622 if (imm != null) imm.restartInput(this); 2623 } else if (type == BufferType.SPANNABLE || mMovement != null) { 2624 text = mSpannableFactory.newSpannable(text); 2625 } else if (!(text instanceof CharWrapper)) { 2626 text = TextUtils.stringOrSpannedString(text); 2627 } 2628 2629 if (mAutoLinkMask != 0) { 2630 Spannable s2; 2631 2632 if (type == BufferType.EDITABLE || text instanceof Spannable) { 2633 s2 = (Spannable) text; 2634 } else { 2635 s2 = mSpannableFactory.newSpannable(text); 2636 } 2637 2638 if (Linkify.addLinks(s2, mAutoLinkMask)) { 2639 text = s2; 2640 type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE; 2641 2642 /* 2643 * We must go ahead and set the text before changing the 2644 * movement method, because setMovementMethod() may call 2645 * setText() again to try to upgrade the buffer type. 2646 */ 2647 mText = text; 2648 2649 if (mLinksClickable) { 2650 setMovementMethod(LinkMovementMethod.getInstance()); 2651 } 2652 } 2653 } 2654 2655 mBufferType = type; 2656 mText = text; 2657 2658 if (mTransformation == null) 2659 mTransformed = text; 2660 else 2661 mTransformed = mTransformation.getTransformation(text, this); 2662 2663 final int textLength = text.length(); 2664 2665 if (text instanceof Spannable) { 2666 Spannable sp = (Spannable) text; 2667 2668 // Remove any ChangeWatchers that might have come 2669 // from other TextViews. 2670 final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class); 2671 final int count = watchers.length; 2672 for (int i = 0; i < count; i++) 2673 sp.removeSpan(watchers[i]); 2674 2675 if (mChangeWatcher == null) 2676 mChangeWatcher = new ChangeWatcher(); 2677 2678 sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE | 2679 (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT)); 2680 2681 if (mInput != null) { 2682 sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); 2683 } 2684 2685 if (mTransformation != null) { 2686 sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); 2687 2688 } 2689 2690 if (mMovement != null) { 2691 mMovement.initialize(this, (Spannable) text); 2692 2693 /* 2694 * Initializing the movement method will have set the 2695 * selection, so reset mSelectionMoved to keep that from 2696 * interfering with the normal on-focus selection-setting. 2697 */ 2698 mSelectionMoved = false; 2699 } 2700 } 2701 2702 if (mLayout != null) { 2703 checkForRelayout(); 2704 } 2705 2706 sendOnTextChanged(text, 0, oldlen, textLength); 2707 onTextChanged(text, 0, oldlen, textLength); 2708 2709 if (needEditableForNotification) { 2710 sendAfterTextChanged((Editable) text); 2711 } 2712 2713 // SelectionModifierCursorController depends on canSelectText, which depends on text 2714 prepareCursorControllers(); 2715 } 2716 2717 /** 2718 * Sets the TextView to display the specified slice of the specified 2719 * char array. You must promise that you will not change the contents 2720 * of the array except for right before another call to setText(), 2721 * since the TextView has no way to know that the text 2722 * has changed and that it needs to invalidate and re-layout. 2723 */ 2724 public final void setText(char[] text, int start, int len) { 2725 int oldlen = 0; 2726 2727 if (start < 0 || len < 0 || start + len > text.length) { 2728 throw new IndexOutOfBoundsException(start + ", " + len); 2729 } 2730 2731 /* 2732 * We must do the before-notification here ourselves because if 2733 * the old text is a CharWrapper we destroy it before calling 2734 * into the normal path. 2735 */ 2736 if (mText != null) { 2737 oldlen = mText.length(); 2738 sendBeforeTextChanged(mText, 0, oldlen, len); 2739 } else { 2740 sendBeforeTextChanged("", 0, 0, len); 2741 } 2742 2743 if (mCharWrapper == null) { 2744 mCharWrapper = new CharWrapper(text, start, len); 2745 } else { 2746 mCharWrapper.set(text, start, len); 2747 } 2748 2749 setText(mCharWrapper, mBufferType, false, oldlen); 2750 } 2751 2752 private static class CharWrapper 2753 implements CharSequence, GetChars, GraphicsOperations { 2754 private char[] mChars; 2755 private int mStart, mLength; 2756 2757 public CharWrapper(char[] chars, int start, int len) { 2758 mChars = chars; 2759 mStart = start; 2760 mLength = len; 2761 } 2762 2763 /* package */ void set(char[] chars, int start, int len) { 2764 mChars = chars; 2765 mStart = start; 2766 mLength = len; 2767 } 2768 2769 public int length() { 2770 return mLength; 2771 } 2772 2773 public char charAt(int off) { 2774 return mChars[off + mStart]; 2775 } 2776 2777 @Override 2778 public String toString() { 2779 return new String(mChars, mStart, mLength); 2780 } 2781 2782 public CharSequence subSequence(int start, int end) { 2783 if (start < 0 || end < 0 || start > mLength || end > mLength) { 2784 throw new IndexOutOfBoundsException(start + ", " + end); 2785 } 2786 2787 return new String(mChars, start + mStart, end - start); 2788 } 2789 2790 public void getChars(int start, int end, char[] buf, int off) { 2791 if (start < 0 || end < 0 || start > mLength || end > mLength) { 2792 throw new IndexOutOfBoundsException(start + ", " + end); 2793 } 2794 2795 System.arraycopy(mChars, start + mStart, buf, off, end - start); 2796 } 2797 2798 public void drawText(Canvas c, int start, int end, 2799 float x, float y, Paint p) { 2800 c.drawText(mChars, start + mStart, end - start, x, y, p); 2801 } 2802 2803 public void drawTextRun(Canvas c, int start, int end, 2804 int contextStart, int contextEnd, float x, float y, int flags, Paint p) { 2805 int count = end - start; 2806 int contextCount = contextEnd - contextStart; 2807 c.drawTextRun(mChars, start + mStart, count, contextStart + mStart, 2808 contextCount, x, y, flags, p); 2809 } 2810 2811 public float measureText(int start, int end, Paint p) { 2812 return p.measureText(mChars, start + mStart, end - start); 2813 } 2814 2815 public int getTextWidths(int start, int end, float[] widths, Paint p) { 2816 return p.getTextWidths(mChars, start + mStart, end - start, widths); 2817 } 2818 2819 public float getTextRunAdvances(int start, int end, int contextStart, 2820 int contextEnd, int flags, float[] advances, int advancesIndex, 2821 Paint p) { 2822 int count = end - start; 2823 int contextCount = contextEnd - contextStart; 2824 return p.getTextRunAdvances(mChars, start + mStart, count, 2825 contextStart + mStart, contextCount, flags, advances, 2826 advancesIndex); 2827 } 2828 2829 public int getTextRunCursor(int contextStart, int contextEnd, int flags, 2830 int offset, int cursorOpt, Paint p) { 2831 int contextCount = contextEnd - contextStart; 2832 return p.getTextRunCursor(mChars, contextStart + mStart, 2833 contextCount, flags, offset + mStart, cursorOpt); 2834 } 2835 } 2836 2837 /** 2838 * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)}, 2839 * except that the cursor position (if any) is retained in the new text. 2840 * 2841 * @see #setText(CharSequence, android.widget.TextView.BufferType) 2842 */ 2843 public final void setTextKeepState(CharSequence text, BufferType type) { 2844 int start = getSelectionStart(); 2845 int end = getSelectionEnd(); 2846 int len = text.length(); 2847 2848 setText(text, type); 2849 2850 if (start >= 0 || end >= 0) { 2851 if (mText instanceof Spannable) { 2852 Selection.setSelection((Spannable) mText, 2853 Math.max(0, Math.min(start, len)), 2854 Math.max(0, Math.min(end, len))); 2855 } 2856 } 2857 } 2858 2859 @android.view.RemotableViewMethod 2860 public final void setText(int resid) { 2861 setText(getContext().getResources().getText(resid)); 2862 } 2863 2864 public final void setText(int resid, BufferType type) { 2865 setText(getContext().getResources().getText(resid), type); 2866 } 2867 2868 /** 2869 * Sets the text to be displayed when the text of the TextView is empty. 2870 * Null means to use the normal empty text. The hint does not currently 2871 * participate in determining the size of the view. 2872 * 2873 * @attr ref android.R.styleable#TextView_hint 2874 */ 2875 @android.view.RemotableViewMethod 2876 public final void setHint(CharSequence hint) { 2877 mHint = TextUtils.stringOrSpannedString(hint); 2878 2879 if (mLayout != null) { 2880 checkForRelayout(); 2881 } 2882 2883 if (mText.length() == 0) { 2884 invalidate(); 2885 } 2886 } 2887 2888 /** 2889 * Sets the text to be displayed when the text of the TextView is empty, 2890 * from a resource. 2891 * 2892 * @attr ref android.R.styleable#TextView_hint 2893 */ 2894 @android.view.RemotableViewMethod 2895 public final void setHint(int resid) { 2896 setHint(getContext().getResources().getText(resid)); 2897 } 2898 2899 /** 2900 * Returns the hint that is displayed when the text of the TextView 2901 * is empty. 2902 * 2903 * @attr ref android.R.styleable#TextView_hint 2904 */ 2905 @ViewDebug.CapturedViewProperty 2906 public CharSequence getHint() { 2907 return mHint; 2908 } 2909 2910 /** 2911 * Set the type of the content with a constant as defined for 2912 * {@link EditorInfo#inputType}. This will take care of changing 2913 * the key listener, by calling {@link #setKeyListener(KeyListener)}, to 2914 * match the given content type. If the given content type is 2915 * {@link EditorInfo#TYPE_NULL} then a soft keyboard will 2916 * not be displayed for this text view. 2917 * 2918 * @see #getInputType() 2919 * @see #setRawInputType(int) 2920 * @see android.text.InputType 2921 * @attr ref android.R.styleable#TextView_inputType 2922 */ 2923 public void setInputType(int type) { 2924 final boolean wasPassword = isPasswordInputType(mInputType); 2925 final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType); 2926 setInputType(type, false); 2927 final boolean isPassword = isPasswordInputType(type); 2928 final boolean isVisiblePassword = isVisiblePasswordInputType(type); 2929 boolean forceUpdate = false; 2930 if (isPassword) { 2931 setTransformationMethod(PasswordTransformationMethod.getInstance()); 2932 setTypefaceByIndex(MONOSPACE, 0); 2933 } else if (isVisiblePassword) { 2934 if (mTransformation == PasswordTransformationMethod.getInstance()) { 2935 forceUpdate = true; 2936 } 2937 setTypefaceByIndex(MONOSPACE, 0); 2938 } else if (wasPassword || wasVisiblePassword) { 2939 // not in password mode, clean up typeface and transformation 2940 setTypefaceByIndex(-1, -1); 2941 if (mTransformation == PasswordTransformationMethod.getInstance()) { 2942 forceUpdate = true; 2943 } 2944 } 2945 2946 boolean multiLine = (type&(EditorInfo.TYPE_MASK_CLASS 2947 | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) == 2948 (EditorInfo.TYPE_CLASS_TEXT 2949 | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); 2950 2951 // We need to update the single line mode if it has changed or we 2952 // were previously in password mode. 2953 if (mSingleLine == multiLine || forceUpdate) { 2954 // Change single line mode, but only change the transformation if 2955 // we are not in password mode. 2956 applySingleLine(!multiLine, !isPassword); 2957 } 2958 2959 InputMethodManager imm = InputMethodManager.peekInstance(); 2960 if (imm != null) imm.restartInput(this); 2961 } 2962 2963 private boolean isPasswordInputType(int inputType) { 2964 final int variation = inputType & (EditorInfo.TYPE_MASK_CLASS 2965 | EditorInfo.TYPE_MASK_VARIATION); 2966 return variation 2967 == (EditorInfo.TYPE_CLASS_TEXT 2968 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD); 2969 } 2970 2971 private boolean isVisiblePasswordInputType(int inputType) { 2972 final int variation = inputType & (EditorInfo.TYPE_MASK_CLASS 2973 | EditorInfo.TYPE_MASK_VARIATION); 2974 return variation 2975 == (EditorInfo.TYPE_CLASS_TEXT 2976 | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); 2977 } 2978 2979 /** 2980 * Directly change the content type integer of the text view, without 2981 * modifying any other state. 2982 * @see #setInputType(int) 2983 * @see android.text.InputType 2984 * @attr ref android.R.styleable#TextView_inputType 2985 */ 2986 public void setRawInputType(int type) { 2987 mInputType = type; 2988 } 2989 2990 private void setInputType(int type, boolean direct) { 2991 final int cls = type & EditorInfo.TYPE_MASK_CLASS; 2992 KeyListener input; 2993 if (cls == EditorInfo.TYPE_CLASS_TEXT) { 2994 boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) 2995 != 0; 2996 TextKeyListener.Capitalize cap; 2997 if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) { 2998 cap = TextKeyListener.Capitalize.CHARACTERS; 2999 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) { 3000 cap = TextKeyListener.Capitalize.WORDS; 3001 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) { 3002 cap = TextKeyListener.Capitalize.SENTENCES; 3003 } else { 3004 cap = TextKeyListener.Capitalize.NONE; 3005 } 3006 input = TextKeyListener.getInstance(autotext, cap); 3007 } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) { 3008 input = DigitsKeyListener.getInstance( 3009 (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0, 3010 (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0); 3011 } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) { 3012 switch (type & EditorInfo.TYPE_MASK_VARIATION) { 3013 case EditorInfo.TYPE_DATETIME_VARIATION_DATE: 3014 input = DateKeyListener.getInstance(); 3015 break; 3016 case EditorInfo.TYPE_DATETIME_VARIATION_TIME: 3017 input = TimeKeyListener.getInstance(); 3018 break; 3019 default: 3020 input = DateTimeKeyListener.getInstance(); 3021 break; 3022 } 3023 } else if (cls == EditorInfo.TYPE_CLASS_PHONE) { 3024 input = DialerKeyListener.getInstance(); 3025 } else { 3026 input = TextKeyListener.getInstance(); 3027 } 3028 setRawInputType(type); 3029 if (direct) mInput = input; 3030 else { 3031 setKeyListenerOnly(input); 3032 } 3033 } 3034 3035 /** 3036 * Get the type of the content. 3037 * 3038 * @see #setInputType(int) 3039 * @see android.text.InputType 3040 */ 3041 public int getInputType() { 3042 return mInputType; 3043 } 3044 3045 /** 3046 * Change the editor type integer associated with the text view, which 3047 * will be reported to an IME with {@link EditorInfo#imeOptions} when it 3048 * has focus. 3049 * @see #getImeOptions 3050 * @see android.view.inputmethod.EditorInfo 3051 * @attr ref android.R.styleable#TextView_imeOptions 3052 */ 3053 public void setImeOptions(int imeOptions) { 3054 if (mInputContentType == null) { 3055 mInputContentType = new InputContentType(); 3056 } 3057 mInputContentType.imeOptions = imeOptions; 3058 } 3059 3060 /** 3061 * Get the type of the IME editor. 3062 * 3063 * @see #setImeOptions(int) 3064 * @see android.view.inputmethod.EditorInfo 3065 */ 3066 public int getImeOptions() { 3067 return mInputContentType != null 3068 ? mInputContentType.imeOptions : EditorInfo.IME_NULL; 3069 } 3070 3071 /** 3072 * Change the custom IME action associated with the text view, which 3073 * will be reported to an IME with {@link EditorInfo#actionLabel} 3074 * and {@link EditorInfo#actionId} when it has focus. 3075 * @see #getImeActionLabel 3076 * @see #getImeActionId 3077 * @see android.view.inputmethod.EditorInfo 3078 * @attr ref android.R.styleable#TextView_imeActionLabel 3079 * @attr ref android.R.styleable#TextView_imeActionId 3080 */ 3081 public void setImeActionLabel(CharSequence label, int actionId) { 3082 if (mInputContentType == null) { 3083 mInputContentType = new InputContentType(); 3084 } 3085 mInputContentType.imeActionLabel = label; 3086 mInputContentType.imeActionId = actionId; 3087 } 3088 3089 /** 3090 * Get the IME action label previous set with {@link #setImeActionLabel}. 3091 * 3092 * @see #setImeActionLabel 3093 * @see android.view.inputmethod.EditorInfo 3094 */ 3095 public CharSequence getImeActionLabel() { 3096 return mInputContentType != null 3097 ? mInputContentType.imeActionLabel : null; 3098 } 3099 3100 /** 3101 * Get the IME action ID previous set with {@link #setImeActionLabel}. 3102 * 3103 * @see #setImeActionLabel 3104 * @see android.view.inputmethod.EditorInfo 3105 */ 3106 public int getImeActionId() { 3107 return mInputContentType != null 3108 ? mInputContentType.imeActionId : 0; 3109 } 3110 3111 /** 3112 * Set a special listener to be called when an action is performed 3113 * on the text view. This will be called when the enter key is pressed, 3114 * or when an action supplied to the IME is selected by the user. Setting 3115 * this means that the normal hard key event will not insert a newline 3116 * into the text view, even if it is multi-line; holding down the ALT 3117 * modifier will, however, allow the user to insert a newline character. 3118 */ 3119 public void setOnEditorActionListener(OnEditorActionListener l) { 3120 if (mInputContentType == null) { 3121 mInputContentType = new InputContentType(); 3122 } 3123 mInputContentType.onEditorActionListener = l; 3124 } 3125 3126 /** 3127 * Called when an attached input method calls 3128 * {@link InputConnection#performEditorAction(int) 3129 * InputConnection.performEditorAction()} 3130 * for this text view. The default implementation will call your action 3131 * listener supplied to {@link #setOnEditorActionListener}, or perform 3132 * a standard operation for {@link EditorInfo#IME_ACTION_NEXT 3133 * EditorInfo.IME_ACTION_NEXT} or {@link EditorInfo#IME_ACTION_DONE 3134 * EditorInfo.IME_ACTION_DONE}. 3135 * 3136 * <p>For backwards compatibility, if no IME options have been set and the 3137 * text view would not normally advance focus on enter, then 3138 * the NEXT and DONE actions received here will be turned into an enter 3139 * key down/up pair to go through the normal key handling. 3140 * 3141 * @param actionCode The code of the action being performed. 3142 * 3143 * @see #setOnEditorActionListener 3144 */ 3145 public void onEditorAction(int actionCode) { 3146 final InputContentType ict = mInputContentType; 3147 if (ict != null) { 3148 if (ict.onEditorActionListener != null) { 3149 if (ict.onEditorActionListener.onEditorAction(this, 3150 actionCode, null)) { 3151 return; 3152 } 3153 } 3154 3155 // This is the handling for some default action. 3156 // Note that for backwards compatibility we don't do this 3157 // default handling if explicit ime options have not been given, 3158 // instead turning this into the normal enter key codes that an 3159 // app may be expecting. 3160 if (actionCode == EditorInfo.IME_ACTION_NEXT) { 3161 View v = focusSearch(FOCUS_DOWN); 3162 if (v != null) { 3163 if (!v.requestFocus(FOCUS_DOWN)) { 3164 throw new IllegalStateException("focus search returned a view " + 3165 "that wasn't able to take focus!"); 3166 } 3167 } 3168 return; 3169 3170 } else if (actionCode == EditorInfo.IME_ACTION_DONE) { 3171 InputMethodManager imm = InputMethodManager.peekInstance(); 3172 if (imm != null) { 3173 imm.hideSoftInputFromWindow(getWindowToken(), 0); 3174 } 3175 return; 3176 } 3177 } 3178 3179 Handler h = getHandler(); 3180 if (h != null) { 3181 long eventTime = SystemClock.uptimeMillis(); 3182 h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, 3183 new KeyEvent(eventTime, eventTime, 3184 KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, 3185 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE 3186 | KeyEvent.FLAG_EDITOR_ACTION))); 3187 h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, 3188 new KeyEvent(SystemClock.uptimeMillis(), eventTime, 3189 KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, 3190 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE 3191 | KeyEvent.FLAG_EDITOR_ACTION))); 3192 } 3193 } 3194 3195 /** 3196 * Set the private content type of the text, which is the 3197 * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions} 3198 * field that will be filled in when creating an input connection. 3199 * 3200 * @see #getPrivateImeOptions() 3201 * @see EditorInfo#privateImeOptions 3202 * @attr ref android.R.styleable#TextView_privateImeOptions 3203 */ 3204 public void setPrivateImeOptions(String type) { 3205 if (mInputContentType == null) mInputContentType = new InputContentType(); 3206 mInputContentType.privateImeOptions = type; 3207 } 3208 3209 /** 3210 * Get the private type of the content. 3211 * 3212 * @see #setPrivateImeOptions(String) 3213 * @see EditorInfo#privateImeOptions 3214 */ 3215 public String getPrivateImeOptions() { 3216 return mInputContentType != null 3217 ? mInputContentType.privateImeOptions : null; 3218 } 3219 3220 /** 3221 * Set the extra input data of the text, which is the 3222 * {@link EditorInfo#extras TextBoxAttribute.extras} 3223 * Bundle that will be filled in when creating an input connection. The 3224 * given integer is the resource ID of an XML resource holding an 3225 * {@link android.R.styleable#InputExtras <input-extras>} XML tree. 3226 * 3227 * @see #getInputExtras(boolean) 3228 * @see EditorInfo#extras 3229 * @attr ref android.R.styleable#TextView_editorExtras 3230 */ 3231 public void setInputExtras(int xmlResId) 3232 throws XmlPullParserException, IOException { 3233 XmlResourceParser parser = getResources().getXml(xmlResId); 3234 if (mInputContentType == null) mInputContentType = new InputContentType(); 3235 mInputContentType.extras = new Bundle(); 3236 getResources().parseBundleExtras(parser, mInputContentType.extras); 3237 } 3238 3239 /** 3240 * Retrieve the input extras currently associated with the text view, which 3241 * can be viewed as well as modified. 3242 * 3243 * @param create If true, the extras will be created if they don't already 3244 * exist. Otherwise, null will be returned if none have been created. 3245 * @see #setInputExtras(int) 3246 * @see EditorInfo#extras 3247 * @attr ref android.R.styleable#TextView_editorExtras 3248 */ 3249 public Bundle getInputExtras(boolean create) { 3250 if (mInputContentType == null) { 3251 if (!create) return null; 3252 mInputContentType = new InputContentType(); 3253 } 3254 if (mInputContentType.extras == null) { 3255 if (!create) return null; 3256 mInputContentType.extras = new Bundle(); 3257 } 3258 return mInputContentType.extras; 3259 } 3260 3261 /** 3262 * Returns the error message that was set to be displayed with 3263 * {@link #setError}, or <code>null</code> if no error was set 3264 * or if it the error was cleared by the widget after user input. 3265 */ 3266 public CharSequence getError() { 3267 return mError; 3268 } 3269 3270 /** 3271 * Sets the right-hand compound drawable of the TextView to the "error" 3272 * icon and sets an error message that will be displayed in a popup when 3273 * the TextView has focus. The icon and error message will be reset to 3274 * null when any key events cause changes to the TextView's text. If the 3275 * <code>error</code> is <code>null</code>, the error message and icon 3276 * will be cleared. 3277 */ 3278 @android.view.RemotableViewMethod 3279 public void setError(CharSequence error) { 3280 if (error == null) { 3281 setError(null, null); 3282 } else { 3283 Drawable dr = getContext().getResources(). 3284 getDrawable(com.android.internal.R.drawable. 3285 indicator_input_error); 3286 3287 dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight()); 3288 setError(error, dr); 3289 } 3290 } 3291 3292 /** 3293 * Sets the right-hand compound drawable of the TextView to the specified 3294 * icon and sets an error message that will be displayed in a popup when 3295 * the TextView has focus. The icon and error message will be reset to 3296 * null when any key events cause changes to the TextView's text. The 3297 * drawable must already have had {@link Drawable#setBounds} set on it. 3298 * If the <code>error</code> is <code>null</code>, the error message will 3299 * be cleared (and you should provide a <code>null</code> icon as well). 3300 */ 3301 public void setError(CharSequence error, Drawable icon) { 3302 error = TextUtils.stringOrSpannedString(error); 3303 3304 mError = error; 3305 mErrorWasChanged = true; 3306 final Drawables dr = mDrawables; 3307 if (dr != null) { 3308 setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, 3309 icon, dr.mDrawableBottom); 3310 } else { 3311 setCompoundDrawables(null, null, icon, null); 3312 } 3313 3314 if (error == null) { 3315 if (mPopup != null) { 3316 if (mPopup.isShowing()) { 3317 mPopup.dismiss(); 3318 } 3319 3320 mPopup = null; 3321 } 3322 } else { 3323 if (isFocused()) { 3324 showError(); 3325 } 3326 } 3327 } 3328 3329 private void showError() { 3330 if (getWindowToken() == null) { 3331 mShowErrorAfterAttach = true; 3332 return; 3333 } 3334 3335 if (mPopup == null) { 3336 LayoutInflater inflater = LayoutInflater.from(getContext()); 3337 final TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint, 3338 null); 3339 3340 final float scale = getResources().getDisplayMetrics().density; 3341 mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), 3342 (int) (50 * scale + 0.5f)); 3343 mPopup.setFocusable(false); 3344 // The user is entering text, so the input method is needed. We 3345 // don't want the popup to be displayed on top of it. 3346 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 3347 } 3348 3349 TextView tv = (TextView) mPopup.getContentView(); 3350 chooseSize(mPopup, mError, tv); 3351 tv.setText(mError); 3352 3353 mPopup.showAsDropDown(this, getErrorX(), getErrorY()); 3354 mPopup.fixDirection(mPopup.isAboveAnchor()); 3355 } 3356 3357 private static class ErrorPopup extends PopupWindow { 3358 private boolean mAbove = false; 3359 private final TextView mView; 3360 3361 ErrorPopup(TextView v, int width, int height) { 3362 super(v, width, height); 3363 mView = v; 3364 } 3365 3366 void fixDirection(boolean above) { 3367 mAbove = above; 3368 3369 if (above) { 3370 mView.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error_above); 3371 } else { 3372 mView.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error); 3373 } 3374 } 3375 3376 @Override 3377 public void update(int x, int y, int w, int h, boolean force) { 3378 super.update(x, y, w, h, force); 3379 3380 boolean above = isAboveAnchor(); 3381 if (above != mAbove) { 3382 fixDirection(above); 3383 } 3384 } 3385 } 3386 3387 /** 3388 * Returns the Y offset to make the pointy top of the error point 3389 * at the middle of the error icon. 3390 */ 3391 private int getErrorX() { 3392 /* 3393 * The "25" is the distance between the point and the right edge 3394 * of the background 3395 */ 3396 final float scale = getResources().getDisplayMetrics().density; 3397 3398 final Drawables dr = mDrawables; 3399 return getWidth() - mPopup.getWidth() 3400 - getPaddingRight() 3401 - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f); 3402 } 3403 3404 /** 3405 * Returns the Y offset to make the pointy top of the error point 3406 * at the bottom of the error icon. 3407 */ 3408 private int getErrorY() { 3409 /* 3410 * Compound, not extended, because the icon is not clipped 3411 * if the text height is smaller. 3412 */ 3413 int vspace = mBottom - mTop - 3414 getCompoundPaddingBottom() - getCompoundPaddingTop(); 3415 3416 final Drawables dr = mDrawables; 3417 int icontop = getCompoundPaddingTop() 3418 + (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2; 3419 3420 /* 3421 * The "2" is the distance between the point and the top edge 3422 * of the background. 3423 */ 3424 3425 return icontop + (dr != null ? dr.mDrawableHeightRight : 0) 3426 - getHeight() - 2; 3427 } 3428 3429 private void hideError() { 3430 if (mPopup != null) { 3431 if (mPopup.isShowing()) { 3432 mPopup.dismiss(); 3433 } 3434 } 3435 3436 mShowErrorAfterAttach = false; 3437 } 3438 3439 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { 3440 int wid = tv.getPaddingLeft() + tv.getPaddingRight(); 3441 int ht = tv.getPaddingTop() + tv.getPaddingBottom(); 3442 3443 /* 3444 * Figure out how big the text would be if we laid it out to the 3445 * full width of this view minus the border. 3446 */ 3447 int cap = getWidth() - wid; 3448 if (cap < 0) { 3449 cap = 200; // We must not be measured yet -- setFrame() will fix it. 3450 } 3451 3452 Layout l = new StaticLayout(text, tv.getPaint(), cap, 3453 Layout.Alignment.ALIGN_NORMAL, 1, 0, true); 3454 float max = 0; 3455 for (int i = 0; i < l.getLineCount(); i++) { 3456 max = Math.max(max, l.getLineWidth(i)); 3457 } 3458 3459 /* 3460 * Now set the popup size to be big enough for the text plus the border. 3461 */ 3462 pop.setWidth(wid + (int) Math.ceil(max)); 3463 pop.setHeight(ht + l.getHeight()); 3464 } 3465 3466 3467 @Override 3468 protected boolean setFrame(int l, int t, int r, int b) { 3469 boolean result = super.setFrame(l, t, r, b); 3470 3471 if (mPopup != null) { 3472 TextView tv = (TextView) mPopup.getContentView(); 3473 chooseSize(mPopup, mError, tv); 3474 mPopup.update(this, getErrorX(), getErrorY(), 3475 mPopup.getWidth(), mPopup.getHeight()); 3476 } 3477 3478 restartMarqueeIfNeeded(); 3479 3480 return result; 3481 } 3482 3483 private void restartMarqueeIfNeeded() { 3484 if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) { 3485 mRestartMarquee = false; 3486 startMarquee(); 3487 } 3488 } 3489 3490 /** 3491 * Sets the list of input filters that will be used if the buffer is 3492 * Editable. Has no effect otherwise. 3493 * 3494 * @attr ref android.R.styleable#TextView_maxLength 3495 */ 3496 public void setFilters(InputFilter[] filters) { 3497 if (filters == null) { 3498 throw new IllegalArgumentException(); 3499 } 3500 3501 mFilters = filters; 3502 3503 if (mText instanceof Editable) { 3504 setFilters((Editable) mText, filters); 3505 } 3506 } 3507 3508 /** 3509 * Sets the list of input filters on the specified Editable, 3510 * and includes mInput in the list if it is an InputFilter. 3511 */ 3512 private void setFilters(Editable e, InputFilter[] filters) { 3513 if (mInput instanceof InputFilter) { 3514 InputFilter[] nf = new InputFilter[filters.length + 1]; 3515 3516 System.arraycopy(filters, 0, nf, 0, filters.length); 3517 nf[filters.length] = (InputFilter) mInput; 3518 3519 e.setFilters(nf); 3520 } else { 3521 e.setFilters(filters); 3522 } 3523 } 3524 3525 /** 3526 * Returns the current list of input filters. 3527 */ 3528 public InputFilter[] getFilters() { 3529 return mFilters; 3530 } 3531 3532 ///////////////////////////////////////////////////////////////////////// 3533 3534 private int getVerticalOffset(boolean forceNormal) { 3535 int voffset = 0; 3536 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 3537 3538 Layout l = mLayout; 3539 if (!forceNormal && mText.length() == 0 && mHintLayout != null) { 3540 l = mHintLayout; 3541 } 3542 3543 if (gravity != Gravity.TOP) { 3544 int boxht; 3545 3546 if (l == mHintLayout) { 3547 boxht = getMeasuredHeight() - getCompoundPaddingTop() - 3548 getCompoundPaddingBottom(); 3549 } else { 3550 boxht = getMeasuredHeight() - getExtendedPaddingTop() - 3551 getExtendedPaddingBottom(); 3552 } 3553 int textht = l.getHeight(); 3554 3555 if (textht < boxht) { 3556 if (gravity == Gravity.BOTTOM) 3557 voffset = boxht - textht; 3558 else // (gravity == Gravity.CENTER_VERTICAL) 3559 voffset = (boxht - textht) >> 1; 3560 } 3561 } 3562 return voffset; 3563 } 3564 3565 private int getBottomVerticalOffset(boolean forceNormal) { 3566 int voffset = 0; 3567 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 3568 3569 Layout l = mLayout; 3570 if (!forceNormal && mText.length() == 0 && mHintLayout != null) { 3571 l = mHintLayout; 3572 } 3573 3574 if (gravity != Gravity.BOTTOM) { 3575 int boxht; 3576 3577 if (l == mHintLayout) { 3578 boxht = getMeasuredHeight() - getCompoundPaddingTop() - 3579 getCompoundPaddingBottom(); 3580 } else { 3581 boxht = getMeasuredHeight() - getExtendedPaddingTop() - 3582 getExtendedPaddingBottom(); 3583 } 3584 int textht = l.getHeight(); 3585 3586 if (textht < boxht) { 3587 if (gravity == Gravity.TOP) 3588 voffset = boxht - textht; 3589 else // (gravity == Gravity.CENTER_VERTICAL) 3590 voffset = (boxht - textht) >> 1; 3591 } 3592 } 3593 return voffset; 3594 } 3595 3596 private void invalidateCursorPath() { 3597 if (mHighlightPathBogus) { 3598 invalidateCursor(); 3599 } else { 3600 synchronized (sTempRect) { 3601 /* 3602 * The reason for this concern about the thickness of the 3603 * cursor and doing the floor/ceil on the coordinates is that 3604 * some EditTexts (notably textfields in the Browser) have 3605 * anti-aliased text where not all the characters are 3606 * necessarily at integer-multiple locations. This should 3607 * make sure the entire cursor gets invalidated instead of 3608 * sometimes missing half a pixel. 3609 */ 3610 3611 float thick = FloatMath.ceil(mTextPaint.getStrokeWidth()); 3612 if (thick < 1.0f) { 3613 thick = 1.0f; 3614 } 3615 3616 thick /= 2; 3617 3618 mHighlightPath.computeBounds(sTempRect, false); 3619 3620 int left = getCompoundPaddingLeft(); 3621 int top = getExtendedPaddingTop() + getVerticalOffset(true); 3622 3623 invalidate((int) FloatMath.floor(left + sTempRect.left - thick), 3624 (int) FloatMath.floor(top + sTempRect.top - thick), 3625 (int) FloatMath.ceil(left + sTempRect.right + thick), 3626 (int) FloatMath.ceil(top + sTempRect.bottom + thick)); 3627 } 3628 } 3629 } 3630 3631 private void invalidateCursor() { 3632 int where = getSelectionEnd(); 3633 3634 invalidateCursor(where, where, where); 3635 } 3636 3637 private void invalidateCursor(int a, int b, int c) { 3638 if (mLayout == null) { 3639 invalidate(); 3640 } else { 3641 if (a >= 0 || b >= 0 || c >= 0) { 3642 int first = Math.min(Math.min(a, b), c); 3643 int last = Math.max(Math.max(a, b), c); 3644 3645 int line = mLayout.getLineForOffset(first); 3646 int top = mLayout.getLineTop(line); 3647 3648 // This is ridiculous, but the descent from the line above 3649 // can hang down into the line we really want to redraw, 3650 // so we have to invalidate part of the line above to make 3651 // sure everything that needs to be redrawn really is. 3652 // (But not the whole line above, because that would cause 3653 // the same problem with the descenders on the line above it!) 3654 if (line > 0) { 3655 top -= mLayout.getLineDescent(line - 1); 3656 } 3657 3658 int line2; 3659 3660 if (first == last) 3661 line2 = line; 3662 else 3663 line2 = mLayout.getLineForOffset(last); 3664 3665 int bottom = mLayout.getLineTop(line2 + 1); 3666 int voffset = getVerticalOffset(true); 3667 3668 int left = getCompoundPaddingLeft() + mScrollX; 3669 invalidate(left, top + voffset + getExtendedPaddingTop(), 3670 left + getWidth() - getCompoundPaddingLeft() - 3671 getCompoundPaddingRight(), 3672 bottom + voffset + getExtendedPaddingTop()); 3673 } 3674 } 3675 } 3676 3677 private void registerForPreDraw() { 3678 final ViewTreeObserver observer = getViewTreeObserver(); 3679 if (observer == null) { 3680 return; 3681 } 3682 3683 if (mPreDrawState == PREDRAW_NOT_REGISTERED) { 3684 observer.addOnPreDrawListener(this); 3685 mPreDrawState = PREDRAW_PENDING; 3686 } else if (mPreDrawState == PREDRAW_DONE) { 3687 mPreDrawState = PREDRAW_PENDING; 3688 } 3689 3690 // else state is PREDRAW_PENDING, so keep waiting. 3691 } 3692 3693 /** 3694 * {@inheritDoc} 3695 */ 3696 public boolean onPreDraw() { 3697 if (mPreDrawState != PREDRAW_PENDING) { 3698 return true; 3699 } 3700 3701 if (mLayout == null) { 3702 assumeLayout(); 3703 } 3704 3705 boolean changed = false; 3706 3707 if (mMovement != null) { 3708 /* This code also provides auto-scrolling when a cursor is moved using a 3709 * CursorController (insertion point or selection limits). 3710 * For selection, ensure start or end is visible depending on controller's state. 3711 */ 3712 int curs = getSelectionEnd(); 3713 if (mSelectionModifierCursorController != null) { 3714 SelectionModifierCursorController selectionController = 3715 (SelectionModifierCursorController) mSelectionModifierCursorController; 3716 if (selectionController.isSelectionStartDragged()) { 3717 curs = getSelectionStart(); 3718 } 3719 } 3720 3721 /* 3722 * TODO: This should really only keep the end in view if 3723 * it already was before the text changed. I'm not sure 3724 * of a good way to tell from here if it was. 3725 */ 3726 if (curs < 0 && 3727 (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { 3728 curs = mText.length(); 3729 } 3730 3731 if (curs >= 0) { 3732 changed = bringPointIntoView(curs); 3733 } 3734 } else { 3735 changed = bringTextIntoView(); 3736 } 3737 3738 if (mShouldStartSelectionActionMode) { 3739 startSelectionActionMode(); 3740 mShouldStartSelectionActionMode = false; 3741 } 3742 mPreDrawState = PREDRAW_DONE; 3743 return !changed; 3744 } 3745 3746 @Override 3747 protected void onAttachedToWindow() { 3748 super.onAttachedToWindow(); 3749 3750 mTemporaryDetach = false; 3751 3752 if (mShowErrorAfterAttach) { 3753 showError(); 3754 mShowErrorAfterAttach = false; 3755 } 3756 } 3757 3758 @Override 3759 protected void onDetachedFromWindow() { 3760 super.onDetachedFromWindow(); 3761 3762 if (mPreDrawState != PREDRAW_NOT_REGISTERED) { 3763 final ViewTreeObserver observer = getViewTreeObserver(); 3764 if (observer != null) { 3765 observer.removeOnPreDrawListener(this); 3766 mPreDrawState = PREDRAW_NOT_REGISTERED; 3767 } 3768 } 3769 3770 if (mError != null) { 3771 hideError(); 3772 } 3773 } 3774 3775 @Override 3776 protected boolean isPaddingOffsetRequired() { 3777 return mShadowRadius != 0 || mDrawables != null; 3778 } 3779 3780 @Override 3781 protected int getLeftPaddingOffset() { 3782 return getCompoundPaddingLeft() - mPaddingLeft + 3783 (int) Math.min(0, mShadowDx - mShadowRadius); 3784 } 3785 3786 @Override 3787 protected int getTopPaddingOffset() { 3788 return (int) Math.min(0, mShadowDy - mShadowRadius); 3789 } 3790 3791 @Override 3792 protected int getBottomPaddingOffset() { 3793 return (int) Math.max(0, mShadowDy + mShadowRadius); 3794 } 3795 3796 @Override 3797 protected int getRightPaddingOffset() { 3798 return -(getCompoundPaddingRight() - mPaddingRight) + 3799 (int) Math.max(0, mShadowDx + mShadowRadius); 3800 } 3801 3802 @Override 3803 protected boolean verifyDrawable(Drawable who) { 3804 final boolean verified = super.verifyDrawable(who); 3805 if (!verified && mDrawables != null) { 3806 return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop || 3807 who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom; 3808 } 3809 return verified; 3810 } 3811 3812 @Override 3813 public void invalidateDrawable(Drawable drawable) { 3814 if (verifyDrawable(drawable)) { 3815 final Rect dirty = drawable.getBounds(); 3816 int scrollX = mScrollX; 3817 int scrollY = mScrollY; 3818 3819 // IMPORTANT: The coordinates below are based on the coordinates computed 3820 // for each compound drawable in onDraw(). Make sure to update each section 3821 // accordingly. 3822 final TextView.Drawables drawables = mDrawables; 3823 if (drawables != null) { 3824 if (drawable == drawables.mDrawableLeft) { 3825 final int compoundPaddingTop = getCompoundPaddingTop(); 3826 final int compoundPaddingBottom = getCompoundPaddingBottom(); 3827 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; 3828 3829 scrollX += mPaddingLeft; 3830 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2; 3831 } else if (drawable == drawables.mDrawableRight) { 3832 final int compoundPaddingTop = getCompoundPaddingTop(); 3833 final int compoundPaddingBottom = getCompoundPaddingBottom(); 3834 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; 3835 3836 scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight); 3837 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2; 3838 } else if (drawable == drawables.mDrawableTop) { 3839 final int compoundPaddingLeft = getCompoundPaddingLeft(); 3840 final int compoundPaddingRight = getCompoundPaddingRight(); 3841 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft; 3842 3843 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2; 3844 scrollY += mPaddingTop; 3845 } else if (drawable == drawables.mDrawableBottom) { 3846 final int compoundPaddingLeft = getCompoundPaddingLeft(); 3847 final int compoundPaddingRight = getCompoundPaddingRight(); 3848 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft; 3849 3850 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2; 3851 scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom); 3852 } 3853 } 3854 3855 invalidate(dirty.left + scrollX, dirty.top + scrollY, 3856 dirty.right + scrollX, dirty.bottom + scrollY); 3857 } 3858 } 3859 3860 @Override 3861 protected boolean onSetAlpha(int alpha) { 3862 if (mMovement == null && getBackground() == null) { 3863 mCurrentAlpha = alpha; 3864 final Drawables dr = mDrawables; 3865 if (dr != null) { 3866 if (dr.mDrawableLeft != null) dr.mDrawableLeft.setAlpha(alpha); 3867 if (dr.mDrawableTop != null) dr.mDrawableTop.setAlpha(alpha); 3868 if (dr.mDrawableRight != null) dr.mDrawableRight.setAlpha(alpha); 3869 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setAlpha(alpha); 3870 } 3871 return true; 3872 } 3873 return false; 3874 } 3875 3876 @Override 3877 protected void onDraw(Canvas canvas) { 3878 restartMarqueeIfNeeded(); 3879 3880 // Draw the background for this view 3881 super.onDraw(canvas); 3882 3883 final int compoundPaddingLeft = getCompoundPaddingLeft(); 3884 final int compoundPaddingTop = getCompoundPaddingTop(); 3885 final int compoundPaddingRight = getCompoundPaddingRight(); 3886 final int compoundPaddingBottom = getCompoundPaddingBottom(); 3887 final int scrollX = mScrollX; 3888 final int scrollY = mScrollY; 3889 final int right = mRight; 3890 final int left = mLeft; 3891 final int bottom = mBottom; 3892 final int top = mTop; 3893 3894 final Drawables dr = mDrawables; 3895 if (dr != null) { 3896 /* 3897 * Compound, not extended, because the icon is not clipped 3898 * if the text height is smaller. 3899 */ 3900 3901 int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop; 3902 int hspace = right - left - compoundPaddingRight - compoundPaddingLeft; 3903 3904 // IMPORTANT: The coordinates computed are also used in invalidateDrawable() 3905 // Make sure to update invalidateDrawable() when changing this code. 3906 if (dr.mDrawableLeft != null) { 3907 canvas.save(); 3908 canvas.translate(scrollX + mPaddingLeft, 3909 scrollY + compoundPaddingTop + 3910 (vspace - dr.mDrawableHeightLeft) / 2); 3911 dr.mDrawableLeft.draw(canvas); 3912 canvas.restore(); 3913 } 3914 3915 // IMPORTANT: The coordinates computed are also used in invalidateDrawable() 3916 // Make sure to update invalidateDrawable() when changing this code. 3917 if (dr.mDrawableRight != null) { 3918 canvas.save(); 3919 canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight, 3920 scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2); 3921 dr.mDrawableRight.draw(canvas); 3922 canvas.restore(); 3923 } 3924 3925 // IMPORTANT: The coordinates computed are also used in invalidateDrawable() 3926 // Make sure to update invalidateDrawable() when changing this code. 3927 if (dr.mDrawableTop != null) { 3928 canvas.save(); 3929 canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2, 3930 scrollY + mPaddingTop); 3931 dr.mDrawableTop.draw(canvas); 3932 canvas.restore(); 3933 } 3934 3935 // IMPORTANT: The coordinates computed are also used in invalidateDrawable() 3936 // Make sure to update invalidateDrawable() when changing this code. 3937 if (dr.mDrawableBottom != null) { 3938 canvas.save(); 3939 canvas.translate(scrollX + compoundPaddingLeft + 3940 (hspace - dr.mDrawableWidthBottom) / 2, 3941 scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom); 3942 dr.mDrawableBottom.draw(canvas); 3943 canvas.restore(); 3944 } 3945 } 3946 3947 if (mPreDrawState == PREDRAW_DONE) { 3948 final ViewTreeObserver observer = getViewTreeObserver(); 3949 if (observer != null) { 3950 observer.removeOnPreDrawListener(this); 3951 mPreDrawState = PREDRAW_NOT_REGISTERED; 3952 } 3953 } 3954 3955 int color = mCurTextColor; 3956 3957 if (mLayout == null) { 3958 assumeLayout(); 3959 } 3960 3961 Layout layout = mLayout; 3962 int cursorcolor = color; 3963 3964 if (mHint != null && mText.length() == 0) { 3965 if (mHintTextColor != null) { 3966 color = mCurHintTextColor; 3967 } 3968 3969 layout = mHintLayout; 3970 } 3971 3972 mTextPaint.setColor(color); 3973 mTextPaint.setAlpha(mCurrentAlpha); 3974 mTextPaint.drawableState = getDrawableState(); 3975 3976 canvas.save(); 3977 /* Would be faster if we didn't have to do this. Can we chop the 3978 (displayable) text so that we don't need to do this ever? 3979 */ 3980 3981 int extendedPaddingTop = getExtendedPaddingTop(); 3982 int extendedPaddingBottom = getExtendedPaddingBottom(); 3983 3984 float clipLeft = compoundPaddingLeft + scrollX; 3985 float clipTop = extendedPaddingTop + scrollY; 3986 float clipRight = right - left - compoundPaddingRight + scrollX; 3987 float clipBottom = bottom - top - extendedPaddingBottom + scrollY; 3988 3989 if (mShadowRadius != 0) { 3990 clipLeft += Math.min(0, mShadowDx - mShadowRadius); 3991 clipRight += Math.max(0, mShadowDx + mShadowRadius); 3992 3993 clipTop += Math.min(0, mShadowDy - mShadowRadius); 3994 clipBottom += Math.max(0, mShadowDy + mShadowRadius); 3995 } 3996 3997 canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom); 3998 3999 int voffsetText = 0; 4000 int voffsetCursor = 0; 4001 4002 // translate in by our padding 4003 { 4004 /* shortcircuit calling getVerticaOffset() */ 4005 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { 4006 voffsetText = getVerticalOffset(false); 4007 voffsetCursor = getVerticalOffset(true); 4008 } 4009 canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); 4010 } 4011 4012 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { 4013 if (!mSingleLine && getLineCount() == 1 && canMarquee() && 4014 (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { 4015 canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft - 4016 getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f); 4017 } 4018 4019 if (mMarquee != null && mMarquee.isRunning()) { 4020 canvas.translate(-mMarquee.mScroll, 0.0f); 4021 } 4022 } 4023 4024 Path highlight = null; 4025 int selStart = -1, selEnd = -1; 4026 4027 // If there is no movement method, then there can be no selection. 4028 // Check that first and attempt to skip everything having to do with 4029 // the cursor. 4030 // XXX This is not strictly true -- a program could set the 4031 // selection manually if it really wanted to. 4032 if (mMovement != null && (isFocused() || isPressed())) { 4033 selStart = getSelectionStart(); 4034 selEnd = getSelectionEnd(); 4035 4036 if (mCursorVisible && selStart >= 0 && isEnabled()) { 4037 if (mHighlightPath == null) 4038 mHighlightPath = new Path(); 4039 4040 if (selStart == selEnd) { 4041 if ((SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) { 4042 if (mHighlightPathBogus) { 4043 mHighlightPath.reset(); 4044 mLayout.getCursorPath(selStart, mHighlightPath, mText); 4045 mHighlightPathBogus = false; 4046 } 4047 4048 // XXX should pass to skin instead of drawing directly 4049 mHighlightPaint.setColor(cursorcolor); 4050 mHighlightPaint.setStyle(Paint.Style.STROKE); 4051 4052 highlight = mHighlightPath; 4053 } 4054 } else { 4055 if (mHighlightPathBogus) { 4056 mHighlightPath.reset(); 4057 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath); 4058 mHighlightPathBogus = false; 4059 } 4060 4061 // XXX should pass to skin instead of drawing directly 4062 mHighlightPaint.setColor(mHighlightColor); 4063 mHighlightPaint.setStyle(Paint.Style.FILL); 4064 4065 highlight = mHighlightPath; 4066 } 4067 } 4068 } 4069 4070 /* Comment out until we decide what to do about animations 4071 boolean isLinearTextOn = false; 4072 if (currentTransformation != null) { 4073 isLinearTextOn = mTextPaint.isLinearTextOn(); 4074 Matrix m = currentTransformation.getMatrix(); 4075 if (!m.isIdentity()) { 4076 // mTextPaint.setLinearTextOn(true); 4077 } 4078 } 4079 */ 4080 4081 final InputMethodState ims = mInputMethodState; 4082 if (ims != null && ims.mBatchEditNesting == 0) { 4083 InputMethodManager imm = InputMethodManager.peekInstance(); 4084 if (imm != null) { 4085 if (imm.isActive(this)) { 4086 boolean reported = false; 4087 if (ims.mContentChanged || ims.mSelectionModeChanged) { 4088 // We are in extract mode and the content has changed 4089 // in some way... just report complete new text to the 4090 // input method. 4091 reported = reportExtractedText(); 4092 } 4093 if (!reported && highlight != null) { 4094 int candStart = -1; 4095 int candEnd = -1; 4096 if (mText instanceof Spannable) { 4097 Spannable sp = (Spannable)mText; 4098 candStart = EditableInputConnection.getComposingSpanStart(sp); 4099 candEnd = EditableInputConnection.getComposingSpanEnd(sp); 4100 } 4101 imm.updateSelection(this, selStart, selEnd, candStart, candEnd); 4102 } 4103 } 4104 4105 if (imm.isWatchingCursor(this) && highlight != null) { 4106 highlight.computeBounds(ims.mTmpRectF, true); 4107 ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0; 4108 4109 canvas.getMatrix().mapPoints(ims.mTmpOffset); 4110 ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]); 4111 4112 ims.mTmpRectF.offset(0, voffsetCursor - voffsetText); 4113 4114 ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5), 4115 (int)(ims.mTmpRectF.top + 0.5), 4116 (int)(ims.mTmpRectF.right + 0.5), 4117 (int)(ims.mTmpRectF.bottom + 0.5)); 4118 4119 imm.updateCursor(this, 4120 ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top, 4121 ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom); 4122 } 4123 } 4124 } 4125 4126 layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText); 4127 4128 if (mMarquee != null && mMarquee.shouldDrawGhost()) { 4129 canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); 4130 layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText); 4131 } 4132 4133 /* Comment out until we decide what to do about animations 4134 if (currentTransformation != null) { 4135 mTextPaint.setLinearTextOn(isLinearTextOn); 4136 } 4137 */ 4138 4139 canvas.restore(); 4140 4141 if (mInsertionPointCursorController != null) { 4142 mInsertionPointCursorController.draw(canvas); 4143 } 4144 if (mSelectionModifierCursorController != null) { 4145 mSelectionModifierCursorController.draw(canvas); 4146 } 4147 } 4148 4149 @Override 4150 public void getFocusedRect(Rect r) { 4151 if (mLayout == null) { 4152 super.getFocusedRect(r); 4153 return; 4154 } 4155 4156 int sel = getSelectionEnd(); 4157 if (sel < 0) { 4158 super.getFocusedRect(r); 4159 return; 4160 } 4161 4162 int line = mLayout.getLineForOffset(sel); 4163 r.top = mLayout.getLineTop(line); 4164 r.bottom = mLayout.getLineBottom(line); 4165 4166 r.left = (int) mLayout.getPrimaryHorizontal(sel); 4167 r.right = r.left + 1; 4168 4169 // Adjust for padding and gravity. 4170 int paddingLeft = getCompoundPaddingLeft(); 4171 int paddingTop = getExtendedPaddingTop(); 4172 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { 4173 paddingTop += getVerticalOffset(false); 4174 } 4175 r.offset(paddingLeft, paddingTop); 4176 } 4177 4178 /** 4179 * Return the number of lines of text, or 0 if the internal Layout has not 4180 * been built. 4181 */ 4182 public int getLineCount() { 4183 return mLayout != null ? mLayout.getLineCount() : 0; 4184 } 4185 4186 /** 4187 * Return the baseline for the specified line (0...getLineCount() - 1) 4188 * If bounds is not null, return the top, left, right, bottom extents 4189 * of the specified line in it. If the internal Layout has not been built, 4190 * return 0 and set bounds to (0, 0, 0, 0) 4191 * @param line which line to examine (0..getLineCount() - 1) 4192 * @param bounds Optional. If not null, it returns the extent of the line 4193 * @return the Y-coordinate of the baseline 4194 */ 4195 public int getLineBounds(int line, Rect bounds) { 4196 if (mLayout == null) { 4197 if (bounds != null) { 4198 bounds.set(0, 0, 0, 0); 4199 } 4200 return 0; 4201 } 4202 else { 4203 int baseline = mLayout.getLineBounds(line, bounds); 4204 4205 int voffset = getExtendedPaddingTop(); 4206 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { 4207 voffset += getVerticalOffset(true); 4208 } 4209 if (bounds != null) { 4210 bounds.offset(getCompoundPaddingLeft(), voffset); 4211 } 4212 return baseline + voffset; 4213 } 4214 } 4215 4216 @Override 4217 public int getBaseline() { 4218 if (mLayout == null) { 4219 return super.getBaseline(); 4220 } 4221 4222 int voffset = 0; 4223 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { 4224 voffset = getVerticalOffset(true); 4225 } 4226 4227 return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0); 4228 } 4229 4230 @Override 4231 public boolean onKeyDown(int keyCode, KeyEvent event) { 4232 int which = doKeyDown(keyCode, event, null); 4233 if (which == 0) { 4234 // Go through default dispatching. 4235 return super.onKeyDown(keyCode, event); 4236 } 4237 4238 return true; 4239 } 4240 4241 @Override 4242 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 4243 KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN); 4244 4245 int which = doKeyDown(keyCode, down, event); 4246 if (which == 0) { 4247 // Go through default dispatching. 4248 return super.onKeyMultiple(keyCode, repeatCount, event); 4249 } 4250 if (which == -1) { 4251 // Consumed the whole thing. 4252 return true; 4253 } 4254 4255 repeatCount--; 4256 4257 // We are going to dispatch the remaining events to either the input 4258 // or movement method. To do this, we will just send a repeated stream 4259 // of down and up events until we have done the complete repeatCount. 4260 // It would be nice if those interfaces had an onKeyMultiple() method, 4261 // but adding that is a more complicated change. 4262 KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP); 4263 if (which == 1) { 4264 mInput.onKeyUp(this, (Editable)mText, keyCode, up); 4265 while (--repeatCount > 0) { 4266 mInput.onKeyDown(this, (Editable)mText, keyCode, down); 4267 mInput.onKeyUp(this, (Editable)mText, keyCode, up); 4268 } 4269 if (mError != null && !mErrorWasChanged) { 4270 setError(null, null); 4271 } 4272 4273 } else if (which == 2) { 4274 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up); 4275 while (--repeatCount > 0) { 4276 mMovement.onKeyDown(this, (Spannable)mText, keyCode, down); 4277 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up); 4278 } 4279 } 4280 4281 return true; 4282 } 4283 4284 /** 4285 * Returns true if pressing ENTER in this field advances focus instead 4286 * of inserting the character. This is true mostly in single-line fields, 4287 * but also in mail addresses and subjects which will display on multiple 4288 * lines but where it doesn't make sense to insert newlines. 4289 */ 4290 private boolean shouldAdvanceFocusOnEnter() { 4291 if (mInput == null) { 4292 return false; 4293 } 4294 4295 if (mSingleLine) { 4296 return true; 4297 } 4298 4299 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { 4300 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION; 4301 4302 if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || 4303 variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) { 4304 return true; 4305 } 4306 } 4307 4308 return false; 4309 } 4310 4311 private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) { 4312 if (!isEnabled()) { 4313 return 0; 4314 } 4315 4316 switch (keyCode) { 4317 case KeyEvent.KEYCODE_ENTER: 4318 // If ALT modifier is held, then we always insert a 4319 // newline character. 4320 if ((event.getMetaState()&KeyEvent.META_ALT_ON) == 0) { 4321 4322 // When mInputContentType is set, we know that we are 4323 // running in a "modern" cupcake environment, so don't need 4324 // to worry about the application trying to capture 4325 // enter key events. 4326 if (mInputContentType != null) { 4327 4328 // If there is an action listener, given them a 4329 // chance to consume the event. 4330 if (mInputContentType.onEditorActionListener != null && 4331 mInputContentType.onEditorActionListener.onEditorAction( 4332 this, EditorInfo.IME_NULL, event)) { 4333 mInputContentType.enterDown = true; 4334 // We are consuming the enter key for them. 4335 return -1; 4336 } 4337 } 4338 4339 // If our editor should move focus when enter is pressed, or 4340 // this is a generated event from an IME action button, then 4341 // don't let it be inserted into the text. 4342 if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0 4343 || shouldAdvanceFocusOnEnter()) { 4344 return -1; 4345 } 4346 } 4347 break; 4348 4349 case KeyEvent.KEYCODE_DPAD_CENTER: 4350 if (shouldAdvanceFocusOnEnter()) { 4351 return 0; 4352 } 4353 } 4354 4355 if (mInput != null) { 4356 /* 4357 * Keep track of what the error was before doing the input 4358 * so that if an input filter changed the error, we leave 4359 * that error showing. Otherwise, we take down whatever 4360 * error was showing when the user types something. 4361 */ 4362 mErrorWasChanged = false; 4363 4364 boolean doDown = true; 4365 if (otherEvent != null) { 4366 try { 4367 beginBatchEdit(); 4368 boolean handled = mInput.onKeyOther(this, (Editable) mText, 4369 otherEvent); 4370 if (mError != null && !mErrorWasChanged) { 4371 setError(null, null); 4372 } 4373 doDown = false; 4374 if (handled) { 4375 return -1; 4376 } 4377 } catch (AbstractMethodError e) { 4378 // onKeyOther was added after 1.0, so if it isn't 4379 // implemented we need to try to dispatch as a regular down. 4380 } finally { 4381 endBatchEdit(); 4382 } 4383 } 4384 4385 if (doDown) { 4386 beginBatchEdit(); 4387 if (mInput.onKeyDown(this, (Editable) mText, keyCode, event)) { 4388 endBatchEdit(); 4389 if (mError != null && !mErrorWasChanged) { 4390 setError(null, null); 4391 } 4392 return 1; 4393 } 4394 endBatchEdit(); 4395 } 4396 } 4397 4398 // bug 650865: sometimes we get a key event before a layout. 4399 // don't try to move around if we don't know the layout. 4400 4401 if (mMovement != null && mLayout != null) { 4402 boolean doDown = true; 4403 if (otherEvent != null) { 4404 try { 4405 boolean handled = mMovement.onKeyOther(this, (Spannable) mText, 4406 otherEvent); 4407 doDown = false; 4408 if (handled) { 4409 return -1; 4410 } 4411 } catch (AbstractMethodError e) { 4412 // onKeyOther was added after 1.0, so if it isn't 4413 // implemented we need to try to dispatch as a regular down. 4414 } 4415 } 4416 if (doDown) { 4417 if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event)) 4418 return 2; 4419 } 4420 } 4421 4422 return 0; 4423 } 4424 4425 @Override 4426 public boolean onKeyUp(int keyCode, KeyEvent event) { 4427 if (!isEnabled()) { 4428 return super.onKeyUp(keyCode, event); 4429 } 4430 4431 hideControllers(); 4432 4433 switch (keyCode) { 4434 case KeyEvent.KEYCODE_DPAD_CENTER: 4435 /* 4436 * If there is a click listener, just call through to 4437 * super, which will invoke it. 4438 * 4439 * If there isn't a click listener, try to show the soft 4440 * input method. (It will also 4441 * call performClick(), but that won't do anything in 4442 * this case.) 4443 */ 4444 if (mOnClickListener == null) { 4445 if (mMovement != null && mText instanceof Editable 4446 && mLayout != null && onCheckIsTextEditor()) { 4447 InputMethodManager imm = (InputMethodManager) 4448 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 4449 imm.showSoftInput(this, 0); 4450 } 4451 } 4452 return super.onKeyUp(keyCode, event); 4453 4454 case KeyEvent.KEYCODE_ENTER: 4455 if (mInputContentType != null 4456 && mInputContentType.onEditorActionListener != null 4457 && mInputContentType.enterDown) { 4458 mInputContentType.enterDown = false; 4459 if (mInputContentType.onEditorActionListener.onEditorAction( 4460 this, EditorInfo.IME_NULL, event)) { 4461 return true; 4462 } 4463 } 4464 4465 if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0 4466 || shouldAdvanceFocusOnEnter()) { 4467 /* 4468 * If there is a click listener, just call through to 4469 * super, which will invoke it. 4470 * 4471 * If there isn't a click listener, try to advance focus, 4472 * but still call through to super, which will reset the 4473 * pressed state and longpress state. (It will also 4474 * call performClick(), but that won't do anything in 4475 * this case.) 4476 */ 4477 if (mOnClickListener == null) { 4478 View v = focusSearch(FOCUS_DOWN); 4479 4480 if (v != null) { 4481 if (!v.requestFocus(FOCUS_DOWN)) { 4482 throw new IllegalStateException("focus search returned a view " + 4483 "that wasn't able to take focus!"); 4484 } 4485 4486 /* 4487 * Return true because we handled the key; super 4488 * will return false because there was no click 4489 * listener. 4490 */ 4491 super.onKeyUp(keyCode, event); 4492 return true; 4493 } else if ((event.getFlags() 4494 & KeyEvent.FLAG_EDITOR_ACTION) != 0) { 4495 // No target for next focus, but make sure the IME 4496 // if this came from it. 4497 InputMethodManager imm = InputMethodManager.peekInstance(); 4498 if (imm != null) { 4499 imm.hideSoftInputFromWindow(getWindowToken(), 0); 4500 } 4501 } 4502 } 4503 4504 return super.onKeyUp(keyCode, event); 4505 } 4506 } 4507 4508 if (mInput != null) 4509 if (mInput.onKeyUp(this, (Editable) mText, keyCode, event)) 4510 return true; 4511 4512 if (mMovement != null && mLayout != null) 4513 if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event)) 4514 return true; 4515 4516 return super.onKeyUp(keyCode, event); 4517 } 4518 4519 @Override public boolean onCheckIsTextEditor() { 4520 return mInputType != EditorInfo.TYPE_NULL; 4521 } 4522 4523 @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 4524 if (onCheckIsTextEditor()) { 4525 if (mInputMethodState == null) { 4526 mInputMethodState = new InputMethodState(); 4527 } 4528 outAttrs.inputType = mInputType; 4529 if (mInputContentType != null) { 4530 outAttrs.imeOptions = mInputContentType.imeOptions; 4531 outAttrs.privateImeOptions = mInputContentType.privateImeOptions; 4532 outAttrs.actionLabel = mInputContentType.imeActionLabel; 4533 outAttrs.actionId = mInputContentType.imeActionId; 4534 outAttrs.extras = mInputContentType.extras; 4535 } else { 4536 outAttrs.imeOptions = EditorInfo.IME_NULL; 4537 } 4538 if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION) 4539 == EditorInfo.IME_ACTION_UNSPECIFIED) { 4540 if (focusSearch(FOCUS_DOWN) != null) { 4541 // An action has not been set, but the enter key will move to 4542 // the next focus, so set the action to that. 4543 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; 4544 } else { 4545 // An action has not been set, and there is no focus to move 4546 // to, so let's just supply a "done" action. 4547 outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE; 4548 } 4549 if (!shouldAdvanceFocusOnEnter()) { 4550 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; 4551 } 4552 } 4553 if ((outAttrs.inputType & (InputType.TYPE_MASK_CLASS 4554 | InputType.TYPE_TEXT_FLAG_MULTI_LINE)) 4555 == (InputType.TYPE_CLASS_TEXT 4556 | InputType.TYPE_TEXT_FLAG_MULTI_LINE)) { 4557 // Multi-line text editors should always show an enter key. 4558 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; 4559 } 4560 outAttrs.hintText = mHint; 4561 if (mText instanceof Editable) { 4562 InputConnection ic = new EditableInputConnection(this); 4563 outAttrs.initialSelStart = getSelectionStart(); 4564 outAttrs.initialSelEnd = getSelectionEnd(); 4565 outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType); 4566 return ic; 4567 } 4568 } 4569 return null; 4570 } 4571 4572 /** 4573 * If this TextView contains editable content, extract a portion of it 4574 * based on the information in <var>request</var> in to <var>outText</var>. 4575 * @return Returns true if the text was successfully extracted, else false. 4576 */ 4577 public boolean extractText(ExtractedTextRequest request, 4578 ExtractedText outText) { 4579 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, 4580 EXTRACT_UNKNOWN, outText); 4581 } 4582 4583 static final int EXTRACT_NOTHING = -2; 4584 static final int EXTRACT_UNKNOWN = -1; 4585 4586 boolean extractTextInternal(ExtractedTextRequest request, 4587 int partialStartOffset, int partialEndOffset, int delta, 4588 ExtractedText outText) { 4589 final CharSequence content = mText; 4590 if (content != null) { 4591 if (partialStartOffset != EXTRACT_NOTHING) { 4592 final int N = content.length(); 4593 if (partialStartOffset < 0) { 4594 outText.partialStartOffset = outText.partialEndOffset = -1; 4595 partialStartOffset = 0; 4596 partialEndOffset = N; 4597 } else { 4598 // Now use the delta to determine the actual amount of text 4599 // we need. 4600 partialEndOffset += delta; 4601 // Adjust offsets to ensure we contain full spans. 4602 if (content instanceof Spanned) { 4603 Spanned spanned = (Spanned)content; 4604 Object[] spans = spanned.getSpans(partialStartOffset, 4605 partialEndOffset, ParcelableSpan.class); 4606 int i = spans.length; 4607 while (i > 0) { 4608 i--; 4609 int j = spanned.getSpanStart(spans[i]); 4610 if (j < partialStartOffset) partialStartOffset = j; 4611 j = spanned.getSpanEnd(spans[i]); 4612 if (j > partialEndOffset) partialEndOffset = j; 4613 } 4614 } 4615 outText.partialStartOffset = partialStartOffset; 4616 outText.partialEndOffset = partialEndOffset - delta; 4617 4618 if (partialStartOffset > N) { 4619 partialStartOffset = N; 4620 } else if (partialStartOffset < 0) { 4621 partialStartOffset = 0; 4622 } 4623 if (partialEndOffset > N) { 4624 partialEndOffset = N; 4625 } else if (partialEndOffset < 0) { 4626 partialEndOffset = 0; 4627 } 4628 } 4629 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { 4630 outText.text = content.subSequence(partialStartOffset, 4631 partialEndOffset); 4632 } else { 4633 outText.text = TextUtils.substring(content, partialStartOffset, 4634 partialEndOffset); 4635 } 4636 } else { 4637 outText.partialStartOffset = 0; 4638 outText.partialEndOffset = 0; 4639 outText.text = ""; 4640 } 4641 outText.flags = 0; 4642 if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) { 4643 outText.flags |= ExtractedText.FLAG_SELECTING; 4644 } 4645 if (mSingleLine) { 4646 outText.flags |= ExtractedText.FLAG_SINGLE_LINE; 4647 } 4648 outText.startOffset = 0; 4649 outText.selectionStart = getSelectionStart(); 4650 outText.selectionEnd = getSelectionEnd(); 4651 return true; 4652 } 4653 return false; 4654 } 4655 4656 boolean reportExtractedText() { 4657 final InputMethodState ims = mInputMethodState; 4658 if (ims != null) { 4659 final boolean contentChanged = ims.mContentChanged; 4660 if (contentChanged || ims.mSelectionModeChanged) { 4661 ims.mContentChanged = false; 4662 ims.mSelectionModeChanged = false; 4663 final ExtractedTextRequest req = mInputMethodState.mExtracting; 4664 if (req != null) { 4665 InputMethodManager imm = InputMethodManager.peekInstance(); 4666 if (imm != null) { 4667 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start=" 4668 + ims.mChangedStart + " end=" + ims.mChangedEnd 4669 + " delta=" + ims.mChangedDelta); 4670 if (ims.mChangedStart < 0 && !contentChanged) { 4671 ims.mChangedStart = EXTRACT_NOTHING; 4672 } 4673 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, 4674 ims.mChangedDelta, ims.mTmpExtracted)) { 4675 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start=" 4676 + ims.mTmpExtracted.partialStartOffset 4677 + " end=" + ims.mTmpExtracted.partialEndOffset 4678 + ": " + ims.mTmpExtracted.text); 4679 imm.updateExtractedText(this, req.token, 4680 mInputMethodState.mTmpExtracted); 4681 ims.mChangedStart = EXTRACT_UNKNOWN; 4682 ims.mChangedEnd = EXTRACT_UNKNOWN; 4683 ims.mChangedDelta = 0; 4684 ims.mContentChanged = false; 4685 return true; 4686 } 4687 } 4688 } 4689 } 4690 } 4691 return false; 4692 } 4693 4694 /** 4695 * This is used to remove all style-impacting spans from text before new 4696 * extracted text is being replaced into it, so that we don't have any 4697 * lingering spans applied during the replace. 4698 */ 4699 static void removeParcelableSpans(Spannable spannable, int start, int end) { 4700 Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class); 4701 int i = spans.length; 4702 while (i > 0) { 4703 i--; 4704 spannable.removeSpan(spans[i]); 4705 } 4706 } 4707 4708 /** 4709 * Apply to this text view the given extracted text, as previously 4710 * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}. 4711 */ 4712 public void setExtractedText(ExtractedText text) { 4713 Editable content = getEditableText(); 4714 if (text.text != null) { 4715 if (content == null) { 4716 setText(text.text, TextView.BufferType.EDITABLE); 4717 } else if (text.partialStartOffset < 0) { 4718 removeParcelableSpans(content, 0, content.length()); 4719 content.replace(0, content.length(), text.text); 4720 } else { 4721 final int N = content.length(); 4722 int start = text.partialStartOffset; 4723 if (start > N) start = N; 4724 int end = text.partialEndOffset; 4725 if (end > N) end = N; 4726 removeParcelableSpans(content, start, end); 4727 content.replace(start, end, text.text); 4728 } 4729 } 4730 4731 // Now set the selection position... make sure it is in range, to 4732 // avoid crashes. If this is a partial update, it is possible that 4733 // the underlying text may have changed, causing us problems here. 4734 // Also we just don't want to trust clients to do the right thing. 4735 Spannable sp = (Spannable)getText(); 4736 final int N = sp.length(); 4737 int start = text.selectionStart; 4738 if (start < 0) start = 0; 4739 else if (start > N) start = N; 4740 int end = text.selectionEnd; 4741 if (end < 0) end = 0; 4742 else if (end > N) end = N; 4743 Selection.setSelection(sp, start, end); 4744 4745 // Finally, update the selection mode. 4746 if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) { 4747 MetaKeyKeyListener.startSelecting(this, sp); 4748 } else { 4749 MetaKeyKeyListener.stopSelecting(this, sp); 4750 } 4751 } 4752 4753 /** 4754 * @hide 4755 */ 4756 public void setExtracting(ExtractedTextRequest req) { 4757 if (mInputMethodState != null) { 4758 mInputMethodState.mExtracting = req; 4759 } 4760 } 4761 4762 /** 4763 * Called by the framework in response to a text completion from 4764 * the current input method, provided by it calling 4765 * {@link InputConnection#commitCompletion 4766 * InputConnection.commitCompletion()}. The default implementation does 4767 * nothing; text views that are supporting auto-completion should override 4768 * this to do their desired behavior. 4769 * 4770 * @param text The auto complete text the user has selected. 4771 */ 4772 public void onCommitCompletion(CompletionInfo text) { 4773 } 4774 4775 public void beginBatchEdit() { 4776 final InputMethodState ims = mInputMethodState; 4777 if (ims != null) { 4778 int nesting = ++ims.mBatchEditNesting; 4779 if (nesting == 1) { 4780 ims.mCursorChanged = false; 4781 ims.mChangedDelta = 0; 4782 if (ims.mContentChanged) { 4783 // We already have a pending change from somewhere else, 4784 // so turn this into a full update. 4785 ims.mChangedStart = 0; 4786 ims.mChangedEnd = mText.length(); 4787 } else { 4788 ims.mChangedStart = EXTRACT_UNKNOWN; 4789 ims.mChangedEnd = EXTRACT_UNKNOWN; 4790 ims.mContentChanged = false; 4791 } 4792 onBeginBatchEdit(); 4793 } 4794 } 4795 } 4796 4797 public void endBatchEdit() { 4798 final InputMethodState ims = mInputMethodState; 4799 if (ims != null) { 4800 int nesting = --ims.mBatchEditNesting; 4801 if (nesting == 0) { 4802 finishBatchEdit(ims); 4803 } 4804 } 4805 } 4806 4807 void ensureEndedBatchEdit() { 4808 final InputMethodState ims = mInputMethodState; 4809 if (ims != null && ims.mBatchEditNesting != 0) { 4810 ims.mBatchEditNesting = 0; 4811 finishBatchEdit(ims); 4812 } 4813 } 4814 4815 void finishBatchEdit(final InputMethodState ims) { 4816 onEndBatchEdit(); 4817 4818 if (ims.mContentChanged || ims.mSelectionModeChanged) { 4819 updateAfterEdit(); 4820 reportExtractedText(); 4821 } else if (ims.mCursorChanged) { 4822 // Cheezy way to get us to report the current cursor location. 4823 invalidateCursor(); 4824 } 4825 } 4826 4827 void updateAfterEdit() { 4828 invalidate(); 4829 int curs = getSelectionStart(); 4830 4831 if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == 4832 Gravity.BOTTOM) { 4833 registerForPreDraw(); 4834 } 4835 4836 if (curs >= 0) { 4837 mHighlightPathBogus = true; 4838 4839 if (isFocused()) { 4840 mShowCursor = SystemClock.uptimeMillis(); 4841 makeBlink(); 4842 } 4843 } 4844 4845 checkForResize(); 4846 } 4847 4848 /** 4849 * Called by the framework in response to a request to begin a batch 4850 * of edit operations through a call to link {@link #beginBatchEdit()}. 4851 */ 4852 public void onBeginBatchEdit() { 4853 } 4854 4855 /** 4856 * Called by the framework in response to a request to end a batch 4857 * of edit operations through a call to link {@link #endBatchEdit}. 4858 */ 4859 public void onEndBatchEdit() { 4860 } 4861 4862 /** 4863 * Called by the framework in response to a private command from the 4864 * current method, provided by it calling 4865 * {@link InputConnection#performPrivateCommand 4866 * InputConnection.performPrivateCommand()}. 4867 * 4868 * @param action The action name of the command. 4869 * @param data Any additional data for the command. This may be null. 4870 * @return Return true if you handled the command, else false. 4871 */ 4872 public boolean onPrivateIMECommand(String action, Bundle data) { 4873 return false; 4874 } 4875 4876 private void nullLayouts() { 4877 if (mLayout instanceof BoringLayout && mSavedLayout == null) { 4878 mSavedLayout = (BoringLayout) mLayout; 4879 } 4880 if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) { 4881 mSavedHintLayout = (BoringLayout) mHintLayout; 4882 } 4883 4884 mLayout = mHintLayout = null; 4885 } 4886 4887 /** 4888 * Make a new Layout based on the already-measured size of the view, 4889 * on the assumption that it was measured correctly at some point. 4890 */ 4891 private void assumeLayout() { 4892 int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); 4893 4894 if (width < 1) { 4895 width = 0; 4896 } 4897 4898 int physicalWidth = width; 4899 4900 if (mHorizontallyScrolling) { 4901 width = VERY_WIDE; 4902 } 4903 4904 makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING, 4905 physicalWidth, false); 4906 } 4907 4908 /** 4909 * The width passed in is now the desired layout width, 4910 * not the full view width with padding. 4911 * {@hide} 4912 */ 4913 protected void makeNewLayout(int w, int hintWidth, 4914 BoringLayout.Metrics boring, 4915 BoringLayout.Metrics hintBoring, 4916 int ellipsisWidth, boolean bringIntoView) { 4917 stopMarquee(); 4918 4919 mHighlightPathBogus = true; 4920 4921 if (w < 0) { 4922 w = 0; 4923 } 4924 if (hintWidth < 0) { 4925 hintWidth = 0; 4926 } 4927 4928 Layout.Alignment alignment; 4929 switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 4930 case Gravity.CENTER_HORIZONTAL: 4931 alignment = Layout.Alignment.ALIGN_CENTER; 4932 break; 4933 4934 case Gravity.RIGHT: 4935 // Note, Layout resolves ALIGN_OPPOSITE to left or 4936 // right based on the paragraph direction. 4937 alignment = Layout.Alignment.ALIGN_OPPOSITE; 4938 break; 4939 4940 default: 4941 alignment = Layout.Alignment.ALIGN_NORMAL; 4942 } 4943 4944 boolean shouldEllipsize = mEllipsize != null && mInput == null; 4945 4946 if (mText instanceof Spannable) { 4947 mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w, 4948 alignment, mSpacingMult, 4949 mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null, 4950 ellipsisWidth); 4951 } else { 4952 if (boring == UNKNOWN_BORING) { 4953 boring = BoringLayout.isBoring(mTransformed, mTextPaint, 4954 mBoring); 4955 if (boring != null) { 4956 mBoring = boring; 4957 } 4958 } 4959 4960 if (boring != null) { 4961 if (boring.width <= w && 4962 (mEllipsize == null || boring.width <= ellipsisWidth)) { 4963 if (mSavedLayout != null) { 4964 mLayout = mSavedLayout. 4965 replaceOrMake(mTransformed, mTextPaint, 4966 w, alignment, mSpacingMult, mSpacingAdd, 4967 boring, mIncludePad); 4968 } else { 4969 mLayout = BoringLayout.make(mTransformed, mTextPaint, 4970 w, alignment, mSpacingMult, mSpacingAdd, 4971 boring, mIncludePad); 4972 } 4973 4974 mSavedLayout = (BoringLayout) mLayout; 4975 } else if (shouldEllipsize && boring.width <= w) { 4976 if (mSavedLayout != null) { 4977 mLayout = mSavedLayout. 4978 replaceOrMake(mTransformed, mTextPaint, 4979 w, alignment, mSpacingMult, mSpacingAdd, 4980 boring, mIncludePad, mEllipsize, 4981 ellipsisWidth); 4982 } else { 4983 mLayout = BoringLayout.make(mTransformed, mTextPaint, 4984 w, alignment, mSpacingMult, mSpacingAdd, 4985 boring, mIncludePad, mEllipsize, 4986 ellipsisWidth); 4987 } 4988 } else if (shouldEllipsize) { 4989 mLayout = new StaticLayout(mTransformed, 4990 0, mTransformed.length(), 4991 mTextPaint, w, alignment, mSpacingMult, 4992 mSpacingAdd, mIncludePad, mEllipsize, 4993 ellipsisWidth); 4994 } else { 4995 mLayout = new StaticLayout(mTransformed, mTextPaint, 4996 w, alignment, mSpacingMult, mSpacingAdd, 4997 mIncludePad); 4998 } 4999 } else if (shouldEllipsize) { 5000 mLayout = new StaticLayout(mTransformed, 5001 0, mTransformed.length(), 5002 mTextPaint, w, alignment, mSpacingMult, 5003 mSpacingAdd, mIncludePad, mEllipsize, 5004 ellipsisWidth); 5005 } else { 5006 mLayout = new StaticLayout(mTransformed, mTextPaint, 5007 w, alignment, mSpacingMult, mSpacingAdd, 5008 mIncludePad); 5009 } 5010 } 5011 5012 shouldEllipsize = mEllipsize != null; 5013 mHintLayout = null; 5014 5015 if (mHint != null) { 5016 if (shouldEllipsize) hintWidth = w; 5017 5018 if (hintBoring == UNKNOWN_BORING) { 5019 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, 5020 mHintBoring); 5021 if (hintBoring != null) { 5022 mHintBoring = hintBoring; 5023 } 5024 } 5025 5026 if (hintBoring != null) { 5027 if (hintBoring.width <= hintWidth && 5028 (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) { 5029 if (mSavedHintLayout != null) { 5030 mHintLayout = mSavedHintLayout. 5031 replaceOrMake(mHint, mTextPaint, 5032 hintWidth, alignment, mSpacingMult, mSpacingAdd, 5033 hintBoring, mIncludePad); 5034 } else { 5035 mHintLayout = BoringLayout.make(mHint, mTextPaint, 5036 hintWidth, alignment, mSpacingMult, mSpacingAdd, 5037 hintBoring, mIncludePad); 5038 } 5039 5040 mSavedHintLayout = (BoringLayout) mHintLayout; 5041 } else if (shouldEllipsize && hintBoring.width <= hintWidth) { 5042 if (mSavedHintLayout != null) { 5043 mHintLayout = mSavedHintLayout. 5044 replaceOrMake(mHint, mTextPaint, 5045 hintWidth, alignment, mSpacingMult, mSpacingAdd, 5046 hintBoring, mIncludePad, mEllipsize, 5047 ellipsisWidth); 5048 } else { 5049 mHintLayout = BoringLayout.make(mHint, mTextPaint, 5050 hintWidth, alignment, mSpacingMult, mSpacingAdd, 5051 hintBoring, mIncludePad, mEllipsize, 5052 ellipsisWidth); 5053 } 5054 } else if (shouldEllipsize) { 5055 mHintLayout = new StaticLayout(mHint, 5056 0, mHint.length(), 5057 mTextPaint, hintWidth, alignment, mSpacingMult, 5058 mSpacingAdd, mIncludePad, mEllipsize, 5059 ellipsisWidth); 5060 } else { 5061 mHintLayout = new StaticLayout(mHint, mTextPaint, 5062 hintWidth, alignment, mSpacingMult, mSpacingAdd, 5063 mIncludePad); 5064 } 5065 } else if (shouldEllipsize) { 5066 mHintLayout = new StaticLayout(mHint, 5067 0, mHint.length(), 5068 mTextPaint, hintWidth, alignment, mSpacingMult, 5069 mSpacingAdd, mIncludePad, mEllipsize, 5070 ellipsisWidth); 5071 } else { 5072 mHintLayout = new StaticLayout(mHint, mTextPaint, 5073 hintWidth, alignment, mSpacingMult, mSpacingAdd, 5074 mIncludePad); 5075 } 5076 } 5077 5078 if (bringIntoView) { 5079 registerForPreDraw(); 5080 } 5081 5082 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { 5083 if (!compressText(ellipsisWidth)) { 5084 final int height = mLayoutParams.height; 5085 // If the size of the view does not depend on the size of the text, try to 5086 // start the marquee immediately 5087 if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) { 5088 startMarquee(); 5089 } else { 5090 // Defer the start of the marquee until we know our width (see setFrame()) 5091 mRestartMarquee = true; 5092 } 5093 } 5094 } 5095 5096 // CursorControllers need a non-null mLayout 5097 prepareCursorControllers(); 5098 } 5099 5100 private boolean compressText(float width) { 5101 // Only compress the text if it hasn't been compressed by the previous pass 5102 if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX && 5103 mTextPaint.getTextScaleX() == 1.0f) { 5104 final float textWidth = mLayout.getLineWidth(0); 5105 final float overflow = (textWidth + 1.0f - width) / width; 5106 if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) { 5107 mTextPaint.setTextScaleX(1.0f - overflow - 0.005f); 5108 post(new Runnable() { 5109 public void run() { 5110 requestLayout(); 5111 } 5112 }); 5113 return true; 5114 } 5115 } 5116 5117 return false; 5118 } 5119 5120 private static int desired(Layout layout) { 5121 int n = layout.getLineCount(); 5122 CharSequence text = layout.getText(); 5123 float max = 0; 5124 5125 // if any line was wrapped, we can't use it. 5126 // but it's ok for the last line not to have a newline 5127 5128 for (int i = 0; i < n - 1; i++) { 5129 if (text.charAt(layout.getLineEnd(i) - 1) != '\n') 5130 return -1; 5131 } 5132 5133 for (int i = 0; i < n; i++) { 5134 max = Math.max(max, layout.getLineWidth(i)); 5135 } 5136 5137 return (int) FloatMath.ceil(max); 5138 } 5139 5140 /** 5141 * Set whether the TextView includes extra top and bottom padding to make 5142 * room for accents that go above the normal ascent and descent. 5143 * The default is true. 5144 * 5145 * @attr ref android.R.styleable#TextView_includeFontPadding 5146 */ 5147 public void setIncludeFontPadding(boolean includepad) { 5148 mIncludePad = includepad; 5149 5150 if (mLayout != null) { 5151 nullLayouts(); 5152 requestLayout(); 5153 invalidate(); 5154 } 5155 } 5156 5157 private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics(); 5158 5159 @Override 5160 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 5161 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 5162 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 5163 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 5164 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 5165 5166 int width; 5167 int height; 5168 5169 BoringLayout.Metrics boring = UNKNOWN_BORING; 5170 BoringLayout.Metrics hintBoring = UNKNOWN_BORING; 5171 5172 int des = -1; 5173 boolean fromexisting = false; 5174 5175 if (widthMode == MeasureSpec.EXACTLY) { 5176 // Parent has told us how big to be. So be it. 5177 width = widthSize; 5178 } else { 5179 if (mLayout != null && mEllipsize == null) { 5180 des = desired(mLayout); 5181 } 5182 5183 if (des < 0) { 5184 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring); 5185 if (boring != null) { 5186 mBoring = boring; 5187 } 5188 } else { 5189 fromexisting = true; 5190 } 5191 5192 if (boring == null || boring == UNKNOWN_BORING) { 5193 if (des < 0) { 5194 des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint)); 5195 } 5196 5197 width = des; 5198 } else { 5199 width = boring.width; 5200 } 5201 5202 final Drawables dr = mDrawables; 5203 if (dr != null) { 5204 width = Math.max(width, dr.mDrawableWidthTop); 5205 width = Math.max(width, dr.mDrawableWidthBottom); 5206 } 5207 5208 if (mHint != null) { 5209 int hintDes = -1; 5210 int hintWidth; 5211 5212 if (mHintLayout != null && mEllipsize == null) { 5213 hintDes = desired(mHintLayout); 5214 } 5215 5216 if (hintDes < 0) { 5217 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring); 5218 if (hintBoring != null) { 5219 mHintBoring = hintBoring; 5220 } 5221 } 5222 5223 if (hintBoring == null || hintBoring == UNKNOWN_BORING) { 5224 if (hintDes < 0) { 5225 hintDes = (int) FloatMath.ceil( 5226 Layout.getDesiredWidth(mHint, mTextPaint)); 5227 } 5228 5229 hintWidth = hintDes; 5230 } else { 5231 hintWidth = hintBoring.width; 5232 } 5233 5234 if (hintWidth > width) { 5235 width = hintWidth; 5236 } 5237 } 5238 5239 width += getCompoundPaddingLeft() + getCompoundPaddingRight(); 5240 5241 if (mMaxWidthMode == EMS) { 5242 width = Math.min(width, mMaxWidth * getLineHeight()); 5243 } else { 5244 width = Math.min(width, mMaxWidth); 5245 } 5246 5247 if (mMinWidthMode == EMS) { 5248 width = Math.max(width, mMinWidth * getLineHeight()); 5249 } else { 5250 width = Math.max(width, mMinWidth); 5251 } 5252 5253 // Check against our minimum width 5254 width = Math.max(width, getSuggestedMinimumWidth()); 5255 5256 if (widthMode == MeasureSpec.AT_MOST) { 5257 width = Math.min(widthSize, width); 5258 } 5259 } 5260 5261 int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight(); 5262 int unpaddedWidth = want; 5263 int hintWant = want; 5264 5265 if (mHorizontallyScrolling) 5266 want = VERY_WIDE; 5267 5268 int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth(); 5269 5270 if (mLayout == null) { 5271 makeNewLayout(want, hintWant, boring, hintBoring, 5272 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); 5273 } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) || 5274 (mLayout.getEllipsizedWidth() != 5275 width - getCompoundPaddingLeft() - getCompoundPaddingRight())) { 5276 if (mHint == null && mEllipsize == null && 5277 want > mLayout.getWidth() && 5278 (mLayout instanceof BoringLayout || 5279 (fromexisting && des >= 0 && des <= want))) { 5280 mLayout.increaseWidthTo(want); 5281 } else { 5282 makeNewLayout(want, hintWant, boring, hintBoring, 5283 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); 5284 } 5285 } else { 5286 // Width has not changed. 5287 } 5288 5289 if (heightMode == MeasureSpec.EXACTLY) { 5290 // Parent has told us how big to be. So be it. 5291 height = heightSize; 5292 mDesiredHeightAtMeasure = -1; 5293 } else { 5294 int desired = getDesiredHeight(); 5295 5296 height = desired; 5297 mDesiredHeightAtMeasure = desired; 5298 5299 if (heightMode == MeasureSpec.AT_MOST) { 5300 height = Math.min(desired, heightSize); 5301 } 5302 } 5303 5304 int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom(); 5305 if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) { 5306 unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum)); 5307 } 5308 5309 /* 5310 * We didn't let makeNewLayout() register to bring the cursor into view, 5311 * so do it here if there is any possibility that it is needed. 5312 */ 5313 if (mMovement != null || 5314 mLayout.getWidth() > unpaddedWidth || 5315 mLayout.getHeight() > unpaddedHeight) { 5316 registerForPreDraw(); 5317 } else { 5318 scrollTo(0, 0); 5319 } 5320 5321 setMeasuredDimension(width, height); 5322 } 5323 5324 private int getDesiredHeight() { 5325 return Math.max( 5326 getDesiredHeight(mLayout, true), 5327 getDesiredHeight(mHintLayout, mEllipsize != null)); 5328 } 5329 5330 private int getDesiredHeight(Layout layout, boolean cap) { 5331 if (layout == null) { 5332 return 0; 5333 } 5334 5335 int linecount = layout.getLineCount(); 5336 int pad = getCompoundPaddingTop() + getCompoundPaddingBottom(); 5337 int desired = layout.getLineTop(linecount); 5338 5339 final Drawables dr = mDrawables; 5340 if (dr != null) { 5341 desired = Math.max(desired, dr.mDrawableHeightLeft); 5342 desired = Math.max(desired, dr.mDrawableHeightRight); 5343 } 5344 5345 desired += pad; 5346 5347 if (mMaxMode == LINES) { 5348 /* 5349 * Don't cap the hint to a certain number of lines. 5350 * (Do cap it, though, if we have a maximum pixel height.) 5351 */ 5352 if (cap) { 5353 if (linecount > mMaximum) { 5354 desired = layout.getLineTop(mMaximum) + 5355 layout.getBottomPadding(); 5356 5357 if (dr != null) { 5358 desired = Math.max(desired, dr.mDrawableHeightLeft); 5359 desired = Math.max(desired, dr.mDrawableHeightRight); 5360 } 5361 5362 desired += pad; 5363 linecount = mMaximum; 5364 } 5365 } 5366 } else { 5367 desired = Math.min(desired, mMaximum); 5368 } 5369 5370 if (mMinMode == LINES) { 5371 if (linecount < mMinimum) { 5372 desired += getLineHeight() * (mMinimum - linecount); 5373 } 5374 } else { 5375 desired = Math.max(desired, mMinimum); 5376 } 5377 5378 // Check against our minimum height 5379 desired = Math.max(desired, getSuggestedMinimumHeight()); 5380 5381 return desired; 5382 } 5383 5384 /** 5385 * Check whether a change to the existing text layout requires a 5386 * new view layout. 5387 */ 5388 private void checkForResize() { 5389 boolean sizeChanged = false; 5390 5391 if (mLayout != null) { 5392 // Check if our width changed 5393 if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) { 5394 sizeChanged = true; 5395 invalidate(); 5396 } 5397 5398 // Check if our height changed 5399 if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) { 5400 int desiredHeight = getDesiredHeight(); 5401 5402 if (desiredHeight != this.getHeight()) { 5403 sizeChanged = true; 5404 } 5405 } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) { 5406 if (mDesiredHeightAtMeasure >= 0) { 5407 int desiredHeight = getDesiredHeight(); 5408 5409 if (desiredHeight != mDesiredHeightAtMeasure) { 5410 sizeChanged = true; 5411 } 5412 } 5413 } 5414 } 5415 5416 if (sizeChanged) { 5417 requestLayout(); 5418 // caller will have already invalidated 5419 } 5420 } 5421 5422 /** 5423 * Check whether entirely new text requires a new view layout 5424 * or merely a new text layout. 5425 */ 5426 private void checkForRelayout() { 5427 // If we have a fixed width, we can just swap in a new text layout 5428 // if the text height stays the same or if the view height is fixed. 5429 5430 if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT || 5431 (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) && 5432 (mHint == null || mHintLayout != null) && 5433 (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) { 5434 // Static width, so try making a new text layout. 5435 5436 int oldht = mLayout.getHeight(); 5437 int want = mLayout.getWidth(); 5438 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth(); 5439 5440 /* 5441 * No need to bring the text into view, since the size is not 5442 * changing (unless we do the requestLayout(), in which case it 5443 * will happen at measure). 5444 */ 5445 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING, 5446 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), 5447 false); 5448 5449 if (mEllipsize != TextUtils.TruncateAt.MARQUEE) { 5450 // In a fixed-height view, so use our new text layout. 5451 if (mLayoutParams.height != LayoutParams.WRAP_CONTENT && 5452 mLayoutParams.height != LayoutParams.MATCH_PARENT) { 5453 invalidate(); 5454 return; 5455 } 5456 5457 // Dynamic height, but height has stayed the same, 5458 // so use our new text layout. 5459 if (mLayout.getHeight() == oldht && 5460 (mHintLayout == null || mHintLayout.getHeight() == oldht)) { 5461 invalidate(); 5462 return; 5463 } 5464 } 5465 5466 // We lose: the height has changed and we have a dynamic height. 5467 // Request a new view layout using our new text layout. 5468 requestLayout(); 5469 invalidate(); 5470 } else { 5471 // Dynamic width, so we have no choice but to request a new 5472 // view layout with a new text layout. 5473 5474 nullLayouts(); 5475 requestLayout(); 5476 invalidate(); 5477 } 5478 } 5479 5480 /** 5481 * Returns true if anything changed. 5482 */ 5483 private boolean bringTextIntoView() { 5484 int line = 0; 5485 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { 5486 line = mLayout.getLineCount() - 1; 5487 } 5488 5489 Layout.Alignment a = mLayout.getParagraphAlignment(line); 5490 int dir = mLayout.getParagraphDirection(line); 5491 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); 5492 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); 5493 int ht = mLayout.getHeight(); 5494 5495 int scrollx, scrolly; 5496 5497 if (a == Layout.Alignment.ALIGN_CENTER) { 5498 /* 5499 * Keep centered if possible, or, if it is too wide to fit, 5500 * keep leading edge in view. 5501 */ 5502 5503 int left = (int) FloatMath.floor(mLayout.getLineLeft(line)); 5504 int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); 5505 5506 if (right - left < hspace) { 5507 scrollx = (right + left) / 2 - hspace / 2; 5508 } else { 5509 if (dir < 0) { 5510 scrollx = right - hspace; 5511 } else { 5512 scrollx = left; 5513 } 5514 } 5515 } else if (a == Layout.Alignment.ALIGN_NORMAL) { 5516 /* 5517 * Keep leading edge in view. 5518 */ 5519 5520 if (dir < 0) { 5521 int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); 5522 scrollx = right - hspace; 5523 } else { 5524 scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); 5525 } 5526 } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ { 5527 /* 5528 * Keep trailing edge in view. 5529 */ 5530 5531 if (dir < 0) { 5532 scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); 5533 } else { 5534 int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); 5535 scrollx = right - hspace; 5536 } 5537 } 5538 5539 if (ht < vspace) { 5540 scrolly = 0; 5541 } else { 5542 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { 5543 scrolly = ht - vspace; 5544 } else { 5545 scrolly = 0; 5546 } 5547 } 5548 5549 if (scrollx != mScrollX || scrolly != mScrollY) { 5550 scrollTo(scrollx, scrolly); 5551 return true; 5552 } else { 5553 return false; 5554 } 5555 } 5556 5557 /** 5558 * Move the point, specified by the offset, into the view if it is needed. 5559 * This has to be called after layout. Returns true if anything changed. 5560 */ 5561 public boolean bringPointIntoView(int offset) { 5562 boolean changed = false; 5563 5564 int line = mLayout.getLineForOffset(offset); 5565 5566 // FIXME: Is it okay to truncate this, or should we round? 5567 final int x = (int)mLayout.getPrimaryHorizontal(offset); 5568 final int top = mLayout.getLineTop(line); 5569 final int bottom = mLayout.getLineTop(line + 1); 5570 5571 int left = (int) FloatMath.floor(mLayout.getLineLeft(line)); 5572 int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); 5573 int ht = mLayout.getHeight(); 5574 5575 int grav; 5576 5577 switch (mLayout.getParagraphAlignment(line)) { 5578 case ALIGN_NORMAL: 5579 grav = 1; 5580 break; 5581 5582 case ALIGN_OPPOSITE: 5583 grav = -1; 5584 break; 5585 5586 default: 5587 grav = 0; 5588 } 5589 5590 grav *= mLayout.getParagraphDirection(line); 5591 5592 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); 5593 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); 5594 5595 int hslack = (bottom - top) / 2; 5596 int vslack = hslack; 5597 5598 if (vslack > vspace / 4) 5599 vslack = vspace / 4; 5600 if (hslack > hspace / 4) 5601 hslack = hspace / 4; 5602 5603 int hs = mScrollX; 5604 int vs = mScrollY; 5605 5606 if (top - vs < vslack) 5607 vs = top - vslack; 5608 if (bottom - vs > vspace - vslack) 5609 vs = bottom - (vspace - vslack); 5610 if (ht - vs < vspace) 5611 vs = ht - vspace; 5612 if (0 - vs > 0) 5613 vs = 0; 5614 5615 if (grav != 0) { 5616 if (x - hs < hslack) { 5617 hs = x - hslack; 5618 } 5619 if (x - hs > hspace - hslack) { 5620 hs = x - (hspace - hslack); 5621 } 5622 } 5623 5624 if (grav < 0) { 5625 if (left - hs > 0) 5626 hs = left; 5627 if (right - hs < hspace) 5628 hs = right - hspace; 5629 } else if (grav > 0) { 5630 if (right - hs < hspace) 5631 hs = right - hspace; 5632 if (left - hs > 0) 5633 hs = left; 5634 } else /* grav == 0 */ { 5635 if (right - left <= hspace) { 5636 /* 5637 * If the entire text fits, center it exactly. 5638 */ 5639 hs = left - (hspace - (right - left)) / 2; 5640 } else if (x > right - hslack) { 5641 /* 5642 * If we are near the right edge, keep the right edge 5643 * at the edge of the view. 5644 */ 5645 hs = right - hspace; 5646 } else if (x < left + hslack) { 5647 /* 5648 * If we are near the left edge, keep the left edge 5649 * at the edge of the view. 5650 */ 5651 hs = left; 5652 } else if (left > hs) { 5653 /* 5654 * Is there whitespace visible at the left? Fix it if so. 5655 */ 5656 hs = left; 5657 } else if (right < hs + hspace) { 5658 /* 5659 * Is there whitespace visible at the right? Fix it if so. 5660 */ 5661 hs = right - hspace; 5662 } else { 5663 /* 5664 * Otherwise, float as needed. 5665 */ 5666 if (x - hs < hslack) { 5667 hs = x - hslack; 5668 } 5669 if (x - hs > hspace - hslack) { 5670 hs = x - (hspace - hslack); 5671 } 5672 } 5673 } 5674 5675 if (hs != mScrollX || vs != mScrollY) { 5676 if (mScroller == null) { 5677 scrollTo(hs, vs); 5678 } else { 5679 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; 5680 int dx = hs - mScrollX; 5681 int dy = vs - mScrollY; 5682 5683 if (duration > ANIMATED_SCROLL_GAP) { 5684 mScroller.startScroll(mScrollX, mScrollY, dx, dy); 5685 awakenScrollBars(mScroller.getDuration()); 5686 invalidate(); 5687 } else { 5688 if (!mScroller.isFinished()) { 5689 mScroller.abortAnimation(); 5690 } 5691 5692 scrollBy(dx, dy); 5693 } 5694 5695 mLastScroll = AnimationUtils.currentAnimationTimeMillis(); 5696 } 5697 5698 changed = true; 5699 } 5700 5701 if (isFocused()) { 5702 // This offsets because getInterestingRect() is in terms of 5703 // viewport coordinates, but requestRectangleOnScreen() 5704 // is in terms of content coordinates. 5705 5706 Rect r = new Rect(x, top, x + 1, bottom); 5707 getInterestingRect(r, line); 5708 r.offset(mScrollX, mScrollY); 5709 5710 if (requestRectangleOnScreen(r)) { 5711 changed = true; 5712 } 5713 } 5714 5715 return changed; 5716 } 5717 5718 /** 5719 * Move the cursor, if needed, so that it is at an offset that is visible 5720 * to the user. This will not move the cursor if it represents more than 5721 * one character (a selection range). This will only work if the 5722 * TextView contains spannable text; otherwise it will do nothing. 5723 * 5724 * @return True if the cursor was actually moved, false otherwise. 5725 */ 5726 public boolean moveCursorToVisibleOffset() { 5727 if (!(mText instanceof Spannable)) { 5728 return false; 5729 } 5730 int start = getSelectionStart(); 5731 int end = getSelectionEnd(); 5732 if (start != end) { 5733 return false; 5734 } 5735 5736 // First: make sure the line is visible on screen: 5737 5738 int line = mLayout.getLineForOffset(start); 5739 5740 final int top = mLayout.getLineTop(line); 5741 final int bottom = mLayout.getLineTop(line + 1); 5742 final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); 5743 int vslack = (bottom - top) / 2; 5744 if (vslack > vspace / 4) 5745 vslack = vspace / 4; 5746 final int vs = mScrollY; 5747 5748 if (top < (vs+vslack)) { 5749 line = mLayout.getLineForVertical(vs+vslack+(bottom-top)); 5750 } else if (bottom > (vspace+vs-vslack)) { 5751 line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top)); 5752 } 5753 5754 // Next: make sure the character is visible on screen: 5755 5756 final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); 5757 final int hs = mScrollX; 5758 final int leftChar = mLayout.getOffsetForHorizontal(line, hs); 5759 final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs); 5760 5761 // line might contain bidirectional text 5762 final int lowChar = leftChar < rightChar ? leftChar : rightChar; 5763 final int highChar = leftChar > rightChar ? leftChar : rightChar; 5764 5765 int newStart = start; 5766 if (newStart < lowChar) { 5767 newStart = lowChar; 5768 } else if (newStart > highChar) { 5769 newStart = highChar; 5770 } 5771 5772 if (newStart != start) { 5773 Selection.setSelection((Spannable)mText, newStart); 5774 return true; 5775 } 5776 5777 return false; 5778 } 5779 5780 @Override 5781 public void computeScroll() { 5782 if (mScroller != null) { 5783 if (mScroller.computeScrollOffset()) { 5784 mScrollX = mScroller.getCurrX(); 5785 mScrollY = mScroller.getCurrY(); 5786 postInvalidate(); // So we draw again 5787 } 5788 } 5789 } 5790 5791 private void getInterestingRect(Rect r, int line) { 5792 convertFromViewportToContentCoordinates(r); 5793 5794 // Rectangle can can be expanded on first and last line to take 5795 // padding into account. 5796 // TODO Take left/right padding into account too? 5797 if (line == 0) r.top -= getExtendedPaddingTop(); 5798 if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom(); 5799 } 5800 5801 private void convertFromViewportToContentCoordinates(Rect r) { 5802 int paddingTop = getExtendedPaddingTop(); 5803 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { 5804 paddingTop += getVerticalOffset(false); 5805 } 5806 r.top += paddingTop; 5807 r.bottom += paddingTop; 5808 5809 int paddingLeft = getCompoundPaddingLeft(); 5810 r.left += paddingLeft; 5811 r.right += paddingLeft; 5812 5813 r.offset(-mScrollX, -mScrollY); 5814 } 5815 5816 @Override 5817 public void debug(int depth) { 5818 super.debug(depth); 5819 5820 String output = debugIndent(depth); 5821 output += "frame={" + mLeft + ", " + mTop + ", " + mRight 5822 + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY 5823 + "} "; 5824 5825 if (mText != null) { 5826 5827 output += "mText=\"" + mText + "\" "; 5828 if (mLayout != null) { 5829 output += "mLayout width=" + mLayout.getWidth() 5830 + " height=" + mLayout.getHeight(); 5831 } 5832 } else { 5833 output += "mText=NULL"; 5834 } 5835 Log.d(VIEW_LOG_TAG, output); 5836 } 5837 5838 /** 5839 * Convenience for {@link Selection#getSelectionStart}. 5840 */ 5841 @ViewDebug.ExportedProperty(category = "text") 5842 public int getSelectionStart() { 5843 return Selection.getSelectionStart(getText()); 5844 } 5845 5846 /** 5847 * Convenience for {@link Selection#getSelectionEnd}. 5848 */ 5849 @ViewDebug.ExportedProperty(category = "text") 5850 public int getSelectionEnd() { 5851 return Selection.getSelectionEnd(getText()); 5852 } 5853 5854 /** 5855 * Return true iff there is a selection inside this text view. 5856 */ 5857 public boolean hasSelection() { 5858 return getSelectionStart() != getSelectionEnd(); 5859 } 5860 5861 /** 5862 * Sets the properties of this field (lines, horizontally scrolling, 5863 * transformation method) to be for a single-line input. 5864 * 5865 * @attr ref android.R.styleable#TextView_singleLine 5866 */ 5867 public void setSingleLine() { 5868 setSingleLine(true); 5869 } 5870 5871 /** 5872 * If true, sets the properties of this field (lines, horizontally 5873 * scrolling, transformation method) to be for a single-line input; 5874 * if false, restores these to the default conditions. 5875 * Note that calling this with false restores default conditions, 5876 * not necessarily those that were in effect prior to calling 5877 * it with true. 5878 * 5879 * @attr ref android.R.styleable#TextView_singleLine 5880 */ 5881 @android.view.RemotableViewMethod 5882 public void setSingleLine(boolean singleLine) { 5883 if ((mInputType&EditorInfo.TYPE_MASK_CLASS) 5884 == EditorInfo.TYPE_CLASS_TEXT) { 5885 if (singleLine) { 5886 mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; 5887 } else { 5888 mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; 5889 } 5890 } 5891 applySingleLine(singleLine, true); 5892 } 5893 5894 private void applySingleLine(boolean singleLine, boolean applyTransformation) { 5895 mSingleLine = singleLine; 5896 if (singleLine) { 5897 setLines(1); 5898 setHorizontallyScrolling(true); 5899 if (applyTransformation) { 5900 setTransformationMethod(SingleLineTransformationMethod. 5901 getInstance()); 5902 } 5903 } else { 5904 setMaxLines(Integer.MAX_VALUE); 5905 setHorizontallyScrolling(false); 5906 if (applyTransformation) { 5907 setTransformationMethod(null); 5908 } 5909 } 5910 } 5911 5912 /** 5913 * Causes words in the text that are longer than the view is wide 5914 * to be ellipsized instead of broken in the middle. You may also 5915 * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling} 5916 * to constrain the text to a single line. Use <code>null</code> 5917 * to turn off ellipsizing. 5918 * 5919 * @attr ref android.R.styleable#TextView_ellipsize 5920 */ 5921 public void setEllipsize(TextUtils.TruncateAt where) { 5922 mEllipsize = where; 5923 5924 if (mLayout != null) { 5925 nullLayouts(); 5926 requestLayout(); 5927 invalidate(); 5928 } 5929 } 5930 5931 /** 5932 * Sets how many times to repeat the marquee animation. Only applied if the 5933 * TextView has marquee enabled. Set to -1 to repeat indefinitely. 5934 * 5935 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit 5936 */ 5937 public void setMarqueeRepeatLimit(int marqueeLimit) { 5938 mMarqueeRepeatLimit = marqueeLimit; 5939 } 5940 5941 /** 5942 * Returns where, if anywhere, words that are longer than the view 5943 * is wide should be ellipsized. 5944 */ 5945 @ViewDebug.ExportedProperty 5946 public TextUtils.TruncateAt getEllipsize() { 5947 return mEllipsize; 5948 } 5949 5950 /** 5951 * Set the TextView so that when it takes focus, all the text is 5952 * selected. 5953 * 5954 * @attr ref android.R.styleable#TextView_selectAllOnFocus 5955 */ 5956 @android.view.RemotableViewMethod 5957 public void setSelectAllOnFocus(boolean selectAllOnFocus) { 5958 mSelectAllOnFocus = selectAllOnFocus; 5959 5960 if (selectAllOnFocus && !(mText instanceof Spannable)) { 5961 setText(mText, BufferType.SPANNABLE); 5962 } 5963 } 5964 5965 /** 5966 * Set whether the cursor is visible. The default is true. 5967 * 5968 * @attr ref android.R.styleable#TextView_cursorVisible 5969 */ 5970 @android.view.RemotableViewMethod 5971 public void setCursorVisible(boolean visible) { 5972 mCursorVisible = visible; 5973 invalidate(); 5974 5975 if (visible) { 5976 makeBlink(); 5977 } else if (mBlink != null) { 5978 mBlink.removeCallbacks(mBlink); 5979 } 5980 5981 // InsertionPointCursorController depends on mCursorVisible 5982 prepareCursorControllers(); 5983 } 5984 5985 private boolean canMarquee() { 5986 int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()); 5987 return width > 0 && mLayout.getLineWidth(0) > width; 5988 } 5989 5990 private void startMarquee() { 5991 // Do not ellipsize EditText 5992 if (mInput != null) return; 5993 5994 if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) { 5995 return; 5996 } 5997 5998 if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) && 5999 getLineCount() == 1 && canMarquee()) { 6000 6001 if (mMarquee == null) mMarquee = new Marquee(this); 6002 mMarquee.start(mMarqueeRepeatLimit); 6003 } 6004 } 6005 6006 private void stopMarquee() { 6007 if (mMarquee != null && !mMarquee.isStopped()) { 6008 mMarquee.stop(); 6009 } 6010 } 6011 6012 private void startStopMarquee(boolean start) { 6013 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { 6014 if (start) { 6015 startMarquee(); 6016 } else { 6017 stopMarquee(); 6018 } 6019 } 6020 } 6021 6022 private static final class Marquee extends Handler { 6023 // TODO: Add an option to configure this 6024 private static final float MARQUEE_DELTA_MAX = 0.07f; 6025 private static final int MARQUEE_DELAY = 1200; 6026 private static final int MARQUEE_RESTART_DELAY = 1200; 6027 private static final int MARQUEE_RESOLUTION = 1000 / 30; 6028 private static final int MARQUEE_PIXELS_PER_SECOND = 30; 6029 6030 private static final byte MARQUEE_STOPPED = 0x0; 6031 private static final byte MARQUEE_STARTING = 0x1; 6032 private static final byte MARQUEE_RUNNING = 0x2; 6033 6034 private static final int MESSAGE_START = 0x1; 6035 private static final int MESSAGE_TICK = 0x2; 6036 private static final int MESSAGE_RESTART = 0x3; 6037 6038 private final WeakReference<TextView> mView; 6039 6040 private byte mStatus = MARQUEE_STOPPED; 6041 private final float mScrollUnit; 6042 private float mMaxScroll; 6043 float mMaxFadeScroll; 6044 private float mGhostStart; 6045 private float mGhostOffset; 6046 private float mFadeStop; 6047 private int mRepeatLimit; 6048 6049 float mScroll; 6050 6051 Marquee(TextView v) { 6052 final float density = v.getContext().getResources().getDisplayMetrics().density; 6053 mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION; 6054 mView = new WeakReference<TextView>(v); 6055 } 6056 6057 @Override 6058 public void handleMessage(Message msg) { 6059 switch (msg.what) { 6060 case MESSAGE_START: 6061 mStatus = MARQUEE_RUNNING; 6062 tick(); 6063 break; 6064 case MESSAGE_TICK: 6065 tick(); 6066 break; 6067 case MESSAGE_RESTART: 6068 if (mStatus == MARQUEE_RUNNING) { 6069 if (mRepeatLimit >= 0) { 6070 mRepeatLimit--; 6071 } 6072 start(mRepeatLimit); 6073 } 6074 break; 6075 } 6076 } 6077 6078 void tick() { 6079 if (mStatus != MARQUEE_RUNNING) { 6080 return; 6081 } 6082 6083 removeMessages(MESSAGE_TICK); 6084 6085 final TextView textView = mView.get(); 6086 if (textView != null && (textView.isFocused() || textView.isSelected())) { 6087 mScroll += mScrollUnit; 6088 if (mScroll > mMaxScroll) { 6089 mScroll = mMaxScroll; 6090 sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY); 6091 } else { 6092 sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION); 6093 } 6094 textView.invalidate(); 6095 } 6096 } 6097 6098 void stop() { 6099 mStatus = MARQUEE_STOPPED; 6100 removeMessages(MESSAGE_START); 6101 removeMessages(MESSAGE_RESTART); 6102 removeMessages(MESSAGE_TICK); 6103 resetScroll(); 6104 } 6105 6106 private void resetScroll() { 6107 mScroll = 0.0f; 6108 final TextView textView = mView.get(); 6109 if (textView != null) textView.invalidate(); 6110 } 6111 6112 void start(int repeatLimit) { 6113 if (repeatLimit == 0) { 6114 stop(); 6115 return; 6116 } 6117 mRepeatLimit = repeatLimit; 6118 final TextView textView = mView.get(); 6119 if (textView != null && textView.mLayout != null) { 6120 mStatus = MARQUEE_STARTING; 6121 mScroll = 0.0f; 6122 final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() - 6123 textView.getCompoundPaddingRight(); 6124 final float lineWidth = textView.mLayout.getLineWidth(0); 6125 final float gap = textWidth / 3.0f; 6126 mGhostStart = lineWidth - textWidth + gap; 6127 mMaxScroll = mGhostStart + textWidth; 6128 mGhostOffset = lineWidth + gap; 6129 mFadeStop = lineWidth + textWidth / 6.0f; 6130 mMaxFadeScroll = mGhostStart + lineWidth + lineWidth; 6131 6132 textView.invalidate(); 6133 sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY); 6134 } 6135 } 6136 6137 float getGhostOffset() { 6138 return mGhostOffset; 6139 } 6140 6141 boolean shouldDrawLeftFade() { 6142 return mScroll <= mFadeStop; 6143 } 6144 6145 boolean shouldDrawGhost() { 6146 return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart; 6147 } 6148 6149 boolean isRunning() { 6150 return mStatus == MARQUEE_RUNNING; 6151 } 6152 6153 boolean isStopped() { 6154 return mStatus == MARQUEE_STOPPED; 6155 } 6156 } 6157 6158 /** 6159 * This method is called when the text is changed, in case any 6160 * subclasses would like to know. 6161 * 6162 * @param text The text the TextView is displaying. 6163 * @param start The offset of the start of the range of the text 6164 * that was modified. 6165 * @param before The offset of the former end of the range of the 6166 * text that was modified. If text was simply inserted, 6167 * this will be the same as <code>start</code>. 6168 * If text was replaced with new text or deleted, the 6169 * length of the old text was <code>before-start</code>. 6170 * @param after The offset of the end of the range of the text 6171 * that was modified. If text was simply deleted, 6172 * this will be the same as <code>start</code>. 6173 * If text was replaced with new text or inserted, 6174 * the length of the new text is <code>after-start</code>. 6175 */ 6176 protected void onTextChanged(CharSequence text, 6177 int start, int before, int after) { 6178 } 6179 6180 /** 6181 * This method is called when the selection has changed, in case any 6182 * subclasses would like to know. 6183 * 6184 * @param selStart The new selection start location. 6185 * @param selEnd The new selection end location. 6186 */ 6187 protected void onSelectionChanged(int selStart, int selEnd) { 6188 } 6189 6190 /** 6191 * Adds a TextWatcher to the list of those whose methods are called 6192 * whenever this TextView's text changes. 6193 * <p> 6194 * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously 6195 * not called after {@link #setText} calls. Now, doing {@link #setText} 6196 * if there are any text changed listeners forces the buffer type to 6197 * Editable if it would not otherwise be and does call this method. 6198 */ 6199 public void addTextChangedListener(TextWatcher watcher) { 6200 if (mListeners == null) { 6201 mListeners = new ArrayList<TextWatcher>(); 6202 } 6203 6204 mListeners.add(watcher); 6205 } 6206 6207 /** 6208 * Removes the specified TextWatcher from the list of those whose 6209 * methods are called 6210 * whenever this TextView's text changes. 6211 */ 6212 public void removeTextChangedListener(TextWatcher watcher) { 6213 if (mListeners != null) { 6214 int i = mListeners.indexOf(watcher); 6215 6216 if (i >= 0) { 6217 mListeners.remove(i); 6218 } 6219 } 6220 } 6221 6222 private void sendBeforeTextChanged(CharSequence text, int start, int before, 6223 int after) { 6224 if (mListeners != null) { 6225 final ArrayList<TextWatcher> list = mListeners; 6226 final int count = list.size(); 6227 for (int i = 0; i < count; i++) { 6228 list.get(i).beforeTextChanged(text, start, before, after); 6229 } 6230 } 6231 } 6232 6233 /** 6234 * Not private so it can be called from an inner class without going 6235 * through a thunk. 6236 */ 6237 void sendOnTextChanged(CharSequence text, int start, int before, 6238 int after) { 6239 if (mListeners != null) { 6240 final ArrayList<TextWatcher> list = mListeners; 6241 final int count = list.size(); 6242 for (int i = 0; i < count; i++) { 6243 list.get(i).onTextChanged(text, start, before, after); 6244 } 6245 } 6246 } 6247 6248 /** 6249 * Not private so it can be called from an inner class without going 6250 * through a thunk. 6251 */ 6252 void sendAfterTextChanged(Editable text) { 6253 if (mListeners != null) { 6254 final ArrayList<TextWatcher> list = mListeners; 6255 final int count = list.size(); 6256 for (int i = 0; i < count; i++) { 6257 list.get(i).afterTextChanged(text); 6258 } 6259 } 6260 } 6261 6262 /** 6263 * Not private so it can be called from an inner class without going 6264 * through a thunk. 6265 */ 6266 void handleTextChanged(CharSequence buffer, int start, 6267 int before, int after) { 6268 final InputMethodState ims = mInputMethodState; 6269 if (ims == null || ims.mBatchEditNesting == 0) { 6270 updateAfterEdit(); 6271 } 6272 if (ims != null) { 6273 ims.mContentChanged = true; 6274 if (ims.mChangedStart < 0) { 6275 ims.mChangedStart = start; 6276 ims.mChangedEnd = start+before; 6277 } else { 6278 ims.mChangedStart = Math.min(ims.mChangedStart, start); 6279 ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta); 6280 } 6281 ims.mChangedDelta += after-before; 6282 } 6283 6284 sendOnTextChanged(buffer, start, before, after); 6285 onTextChanged(buffer, start, before, after); 6286 hideControllers(); 6287 } 6288 6289 /** 6290 * Not private so it can be called from an inner class without going 6291 * through a thunk. 6292 */ 6293 void spanChange(Spanned buf, Object what, int oldStart, int newStart, 6294 int oldEnd, int newEnd) { 6295 // XXX Make the start and end move together if this ends up 6296 // spending too much time invalidating. 6297 6298 boolean selChanged = false; 6299 int newSelStart=-1, newSelEnd=-1; 6300 6301 final InputMethodState ims = mInputMethodState; 6302 6303 if (what == Selection.SELECTION_END) { 6304 mHighlightPathBogus = true; 6305 selChanged = true; 6306 newSelEnd = newStart; 6307 6308 if (!isFocused()) { 6309 mSelectionMoved = true; 6310 } 6311 6312 if (oldStart >= 0 || newStart >= 0) { 6313 invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart); 6314 registerForPreDraw(); 6315 6316 if (isFocused()) { 6317 mShowCursor = SystemClock.uptimeMillis(); 6318 makeBlink(); 6319 } 6320 } 6321 } 6322 6323 if (what == Selection.SELECTION_START) { 6324 mHighlightPathBogus = true; 6325 selChanged = true; 6326 newSelStart = newStart; 6327 6328 if (!isFocused()) { 6329 mSelectionMoved = true; 6330 } 6331 6332 if (oldStart >= 0 || newStart >= 0) { 6333 int end = Selection.getSelectionEnd(buf); 6334 invalidateCursor(end, oldStart, newStart); 6335 } 6336 } 6337 6338 if (selChanged) { 6339 if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) { 6340 if (newSelStart < 0) { 6341 newSelStart = Selection.getSelectionStart(buf); 6342 } 6343 if (newSelEnd < 0) { 6344 newSelEnd = Selection.getSelectionEnd(buf); 6345 } 6346 onSelectionChanged(newSelStart, newSelEnd); 6347 } 6348 } 6349 6350 if (what instanceof UpdateAppearance || 6351 what instanceof ParagraphStyle) { 6352 if (ims == null || ims.mBatchEditNesting == 0) { 6353 invalidate(); 6354 mHighlightPathBogus = true; 6355 checkForResize(); 6356 } else { 6357 ims.mContentChanged = true; 6358 } 6359 } 6360 6361 if (MetaKeyKeyListener.isMetaTracker(buf, what)) { 6362 mHighlightPathBogus = true; 6363 if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) { 6364 ims.mSelectionModeChanged = true; 6365 } 6366 6367 if (Selection.getSelectionStart(buf) >= 0) { 6368 if (ims == null || ims.mBatchEditNesting == 0) { 6369 invalidateCursor(); 6370 } else { 6371 ims.mCursorChanged = true; 6372 } 6373 } 6374 } 6375 6376 if (what instanceof ParcelableSpan) { 6377 // If this is a span that can be sent to a remote process, 6378 // the current extract editor would be interested in it. 6379 if (ims != null && ims.mExtracting != null) { 6380 if (ims.mBatchEditNesting != 0) { 6381 if (oldStart >= 0) { 6382 if (ims.mChangedStart > oldStart) { 6383 ims.mChangedStart = oldStart; 6384 } 6385 if (ims.mChangedStart > oldEnd) { 6386 ims.mChangedStart = oldEnd; 6387 } 6388 } 6389 if (newStart >= 0) { 6390 if (ims.mChangedStart > newStart) { 6391 ims.mChangedStart = newStart; 6392 } 6393 if (ims.mChangedStart > newEnd) { 6394 ims.mChangedStart = newEnd; 6395 } 6396 } 6397 } else { 6398 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: " 6399 + oldStart + "-" + oldEnd + "," 6400 + newStart + "-" + newEnd + what); 6401 ims.mContentChanged = true; 6402 } 6403 } 6404 } 6405 } 6406 6407 private class ChangeWatcher 6408 implements TextWatcher, SpanWatcher { 6409 6410 private CharSequence mBeforeText; 6411 6412 public void beforeTextChanged(CharSequence buffer, int start, 6413 int before, int after) { 6414 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start 6415 + " before=" + before + " after=" + after + ": " + buffer); 6416 6417 if (AccessibilityManager.getInstance(mContext).isEnabled() 6418 && !isPasswordInputType(mInputType)) { 6419 mBeforeText = buffer.toString(); 6420 } 6421 6422 TextView.this.sendBeforeTextChanged(buffer, start, before, after); 6423 } 6424 6425 public void onTextChanged(CharSequence buffer, int start, 6426 int before, int after) { 6427 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start 6428 + " before=" + before + " after=" + after + ": " + buffer); 6429 TextView.this.handleTextChanged(buffer, start, before, after); 6430 6431 if (AccessibilityManager.getInstance(mContext).isEnabled() && 6432 (isFocused() || isSelected() && 6433 isShown())) { 6434 sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after); 6435 mBeforeText = null; 6436 } 6437 6438 // TODO. The cursor controller should hide as soon as text is typed. 6439 // But this method is also used for cosmetic changes (underline current word when 6440 // spell corrections are displayed. There is currently no way to make the difference 6441 // between these cosmetic changes and actual text modifications. 6442 } 6443 6444 public void afterTextChanged(Editable buffer) { 6445 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer); 6446 TextView.this.sendAfterTextChanged(buffer); 6447 6448 if (MetaKeyKeyListener.getMetaState(buffer, 6449 MetaKeyKeyListener.META_SELECTING) != 0) { 6450 MetaKeyKeyListener.stopSelecting(TextView.this, buffer); 6451 } 6452 } 6453 6454 public void onSpanChanged(Spannable buf, 6455 Object what, int s, int e, int st, int en) { 6456 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e 6457 + " st=" + st + " en=" + en + " what=" + what + ": " + buf); 6458 TextView.this.spanChange(buf, what, s, st, e, en); 6459 } 6460 6461 public void onSpanAdded(Spannable buf, Object what, int s, int e) { 6462 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e 6463 + " what=" + what + ": " + buf); 6464 TextView.this.spanChange(buf, what, -1, s, -1, e); 6465 } 6466 6467 public void onSpanRemoved(Spannable buf, Object what, int s, int e) { 6468 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e 6469 + " what=" + what + ": " + buf); 6470 TextView.this.spanChange(buf, what, s, -1, e, -1); 6471 } 6472 } 6473 6474 private void makeBlink() { 6475 if (!mCursorVisible) { 6476 if (mBlink != null) { 6477 mBlink.removeCallbacks(mBlink); 6478 } 6479 6480 return; 6481 } 6482 6483 if (mBlink == null) 6484 mBlink = new Blink(this); 6485 6486 mBlink.removeCallbacks(mBlink); 6487 mBlink.postAtTime(mBlink, mShowCursor + BLINK); 6488 } 6489 6490 /** 6491 * @hide 6492 */ 6493 @Override 6494 public void dispatchFinishTemporaryDetach() { 6495 mDispatchTemporaryDetach = true; 6496 super.dispatchFinishTemporaryDetach(); 6497 mDispatchTemporaryDetach = false; 6498 } 6499 6500 @Override 6501 public void onStartTemporaryDetach() { 6502 super.onStartTemporaryDetach(); 6503 // Only track when onStartTemporaryDetach() is called directly, 6504 // usually because this instance is an editable field in a list 6505 if (!mDispatchTemporaryDetach) mTemporaryDetach = true; 6506 } 6507 6508 @Override 6509 public void onFinishTemporaryDetach() { 6510 super.onFinishTemporaryDetach(); 6511 // Only track when onStartTemporaryDetach() is called directly, 6512 // usually because this instance is an editable field in a list 6513 if (!mDispatchTemporaryDetach) mTemporaryDetach = false; 6514 } 6515 6516 @Override 6517 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 6518 if (mTemporaryDetach) { 6519 // If we are temporarily in the detach state, then do nothing. 6520 super.onFocusChanged(focused, direction, previouslyFocusedRect); 6521 return; 6522 } 6523 6524 mShowCursor = SystemClock.uptimeMillis(); 6525 6526 ensureEndedBatchEdit(); 6527 6528 if (focused) { 6529 int selStart = getSelectionStart(); 6530 int selEnd = getSelectionEnd(); 6531 6532 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) { 6533 boolean selMoved = mSelectionMoved; 6534 6535 if (mSelectionModifierCursorController != null) { 6536 final int touchOffset = 6537 ((SelectionModifierCursorController) mSelectionModifierCursorController). 6538 getMinTouchOffset(); 6539 Selection.setSelection((Spannable) mText, touchOffset); 6540 } 6541 6542 if (mMovement != null) { 6543 mMovement.onTakeFocus(this, (Spannable) mText, direction); 6544 } 6545 6546 if (mSelectAllOnFocus) { 6547 Selection.setSelection((Spannable) mText, 0, mText.length()); 6548 } 6549 6550 if (selMoved && selStart >= 0 && selEnd >= 0) { 6551 /* 6552 * Someone intentionally set the selection, so let them 6553 * do whatever it is that they wanted to do instead of 6554 * the default on-focus behavior. We reset the selection 6555 * here instead of just skipping the onTakeFocus() call 6556 * because some movement methods do something other than 6557 * just setting the selection in theirs and we still 6558 * need to go through that path. 6559 */ 6560 6561 Selection.setSelection((Spannable) mText, selStart, selEnd); 6562 } 6563 mTouchFocusSelected = true; 6564 } 6565 6566 mFrozenWithFocus = false; 6567 mSelectionMoved = false; 6568 6569 if (mText instanceof Spannable) { 6570 Spannable sp = (Spannable) mText; 6571 MetaKeyKeyListener.resetMetaState(sp); 6572 } 6573 6574 makeBlink(); 6575 6576 if (mError != null) { 6577 showError(); 6578 } 6579 6580 // We cannot start the selection mode immediately. The layout may be null here and is 6581 // needed by the cursor controller. Layout creation is deferred up to drawing. The 6582 // selection action mode will be started in onPreDraw(). 6583 if (selStart != selEnd) { 6584 mShouldStartSelectionActionMode = true; 6585 } 6586 } else { 6587 if (mError != null) { 6588 hideError(); 6589 } 6590 // Don't leave us in the middle of a batch edit. 6591 onEndBatchEdit(); 6592 6593 hideInsertionPointCursorController(); 6594 terminateSelectionActionMode(); 6595 } 6596 6597 startStopMarquee(focused); 6598 6599 if (mTransformation != null) { 6600 mTransformation.onFocusChanged(this, mText, focused, direction, 6601 previouslyFocusedRect); 6602 } 6603 6604 super.onFocusChanged(focused, direction, previouslyFocusedRect); 6605 } 6606 6607 @Override 6608 public void onWindowFocusChanged(boolean hasWindowFocus) { 6609 super.onWindowFocusChanged(hasWindowFocus); 6610 6611 if (hasWindowFocus) { 6612 if (mBlink != null) { 6613 mBlink.uncancel(); 6614 6615 if (isFocused()) { 6616 mShowCursor = SystemClock.uptimeMillis(); 6617 makeBlink(); 6618 } 6619 } 6620 } else { 6621 if (mBlink != null) { 6622 mBlink.cancel(); 6623 } 6624 // Don't leave us in the middle of a batch edit. 6625 onEndBatchEdit(); 6626 if (mInputContentType != null) { 6627 mInputContentType.enterDown = false; 6628 } 6629 } 6630 6631 startStopMarquee(hasWindowFocus); 6632 } 6633 6634 /** 6635 * Use {@link BaseInputConnection#removeComposingSpans 6636 * BaseInputConnection.removeComposingSpans()} to remove any IME composing 6637 * state from this text view. 6638 */ 6639 public void clearComposingText() { 6640 if (mText instanceof Spannable) { 6641 BaseInputConnection.removeComposingSpans((Spannable)mText); 6642 } 6643 } 6644 6645 @Override 6646 public void setSelected(boolean selected) { 6647 boolean wasSelected = isSelected(); 6648 6649 super.setSelected(selected); 6650 6651 if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) { 6652 if (selected) { 6653 startMarquee(); 6654 } else { 6655 stopMarquee(); 6656 } 6657 } 6658 } 6659 6660 class CommitSelectionReceiver extends ResultReceiver { 6661 private final int mPrevStart, mPrevEnd; 6662 private final int mNewStart, mNewEnd; 6663 6664 public CommitSelectionReceiver(int mPrevStart, int mPrevEnd, int mNewStart, int mNewEnd) { 6665 super(getHandler()); 6666 this.mPrevStart = mPrevStart; 6667 this.mPrevEnd = mPrevEnd; 6668 this.mNewStart = mNewStart; 6669 this.mNewEnd = mNewEnd; 6670 } 6671 6672 @Override 6673 protected void onReceiveResult(int resultCode, Bundle resultData) { 6674 int start = mNewStart; 6675 int end = mNewEnd; 6676 6677 // Move the cursor to the new position, unless this tap was actually 6678 // use to show the IMM. Leave cursor unchanged in that case. 6679 if (resultCode == InputMethodManager.RESULT_SHOWN) { 6680 start = mPrevStart; 6681 end = mPrevEnd; 6682 } else { 6683 if ((mPrevStart != mPrevEnd) && (start == end)) { 6684 if ((start >= mPrevStart) && (start <= mPrevEnd)) { 6685 // Tapping inside the selection does nothing 6686 Selection.setSelection((Spannable) mText, mPrevStart, mPrevEnd); 6687 return; 6688 } else { 6689 // Tapping outside stops selection mode, if any 6690 stopSelectionActionMode(); 6691 } 6692 } 6693 6694 if (mInsertionPointCursorController != null) { 6695 mInsertionPointCursorController.show(); 6696 } 6697 } 6698 6699 final int len = mText.length(); 6700 if (start > len) { 6701 start = len; 6702 } 6703 if (end > len) { 6704 end = len; 6705 } 6706 Selection.setSelection((Spannable)mText, start, end); 6707 } 6708 } 6709 6710 @Override 6711 public boolean onTouchEvent(MotionEvent event) { 6712 final int action = event.getAction(); 6713 if (action == MotionEvent.ACTION_DOWN) { 6714 // Reset this state; it will be re-set if super.onTouchEvent 6715 // causes focus to move to the view. 6716 mTouchFocusSelected = false; 6717 mScrolled = false; 6718 } 6719 6720 final boolean superResult = super.onTouchEvent(event); 6721 6722 /* 6723 * Don't handle the release after a long press, because it will 6724 * move the selection away from whatever the menu action was 6725 * trying to affect. 6726 */ 6727 if (mEatTouchRelease && action == MotionEvent.ACTION_UP) { 6728 mEatTouchRelease = false; 6729 return superResult; 6730 } 6731 6732 if ((mMovement != null || onCheckIsTextEditor()) && 6733 mText instanceof Spannable && mLayout != null) { 6734 6735 int oldSelStart = getSelectionStart(); 6736 int oldSelEnd = getSelectionEnd(); 6737 6738 if (mInsertionPointCursorController != null) { 6739 mInsertionPointCursorController.onTouchEvent(event); 6740 } 6741 if (mSelectionModifierCursorController != null) { 6742 mSelectionModifierCursorController.onTouchEvent(event); 6743 } 6744 6745 boolean handled = false; 6746 6747 if (mMovement != null) { 6748 handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); 6749 } 6750 6751 if (isTextEditable()) { 6752 if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) { 6753 InputMethodManager imm = (InputMethodManager) 6754 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 6755 6756 final int newSelStart = getSelectionStart(); 6757 final int newSelEnd = getSelectionEnd(); 6758 6759 CommitSelectionReceiver csr = null; 6760 if (newSelStart != oldSelStart || newSelEnd != oldSelEnd || 6761 didTouchFocusSelect()) { 6762 csr = new CommitSelectionReceiver(oldSelStart, oldSelEnd, 6763 newSelStart, newSelEnd); 6764 } 6765 6766 handled |= imm.showSoftInput(this, 0, csr) && (csr != null); 6767 } 6768 } 6769 6770 if (handled) { 6771 return true; 6772 } 6773 } 6774 6775 return superResult; 6776 } 6777 6778 private void prepareCursorControllers() { 6779 // TODO Add an extra android:cursorController flag to disable the controller? 6780 if (mCursorVisible && mLayout != null) { 6781 if (mInsertionPointCursorController == null) { 6782 mInsertionPointCursorController = new InsertionPointCursorController(); 6783 } 6784 } else { 6785 mInsertionPointCursorController = null; 6786 } 6787 6788 if (canSelectText() && mLayout != null) { 6789 if (mSelectionModifierCursorController == null) { 6790 mSelectionModifierCursorController = new SelectionModifierCursorController(); 6791 } 6792 } else { 6793 // Stop selection mode if the controller becomes unavailable. 6794 stopSelectionActionMode(); 6795 mSelectionModifierCursorController = null; 6796 } 6797 } 6798 6799 /** 6800 * @return True iff this TextView contains a text that can be edited. 6801 */ 6802 private boolean isTextEditable() { 6803 return mText instanceof Editable && onCheckIsTextEditor(); 6804 } 6805 6806 /** 6807 * Returns true, only while processing a touch gesture, if the initial 6808 * touch down event caused focus to move to the text view and as a result 6809 * its selection changed. Only valid while processing the touch gesture 6810 * of interest. 6811 */ 6812 public boolean didTouchFocusSelect() { 6813 return mTouchFocusSelected; 6814 } 6815 6816 @Override 6817 public void cancelLongPress() { 6818 super.cancelLongPress(); 6819 mScrolled = true; 6820 } 6821 6822 @Override 6823 public boolean onTrackballEvent(MotionEvent event) { 6824 if (mMovement != null && mText instanceof Spannable && 6825 mLayout != null) { 6826 if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) { 6827 return true; 6828 } 6829 } 6830 6831 return super.onTrackballEvent(event); 6832 } 6833 6834 public void setScroller(Scroller s) { 6835 mScroller = s; 6836 } 6837 6838 private static class Blink extends Handler implements Runnable { 6839 private final WeakReference<TextView> mView; 6840 private boolean mCancelled; 6841 6842 public Blink(TextView v) { 6843 mView = new WeakReference<TextView>(v); 6844 } 6845 6846 public void run() { 6847 if (mCancelled) { 6848 return; 6849 } 6850 6851 removeCallbacks(Blink.this); 6852 6853 TextView tv = mView.get(); 6854 6855 if (tv != null && tv.isFocused()) { 6856 int st = tv.getSelectionStart(); 6857 int en = tv.getSelectionEnd(); 6858 6859 if (st == en && st >= 0 && en >= 0) { 6860 if (tv.mLayout != null) { 6861 tv.invalidateCursorPath(); 6862 } 6863 6864 postAtTime(this, SystemClock.uptimeMillis() + BLINK); 6865 } 6866 } 6867 } 6868 6869 void cancel() { 6870 if (!mCancelled) { 6871 removeCallbacks(Blink.this); 6872 mCancelled = true; 6873 } 6874 } 6875 6876 void uncancel() { 6877 mCancelled = false; 6878 } 6879 } 6880 6881 @Override 6882 protected float getLeftFadingEdgeStrength() { 6883 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { 6884 if (mMarquee != null && !mMarquee.isStopped()) { 6885 final Marquee marquee = mMarquee; 6886 if (marquee.shouldDrawLeftFade()) { 6887 return marquee.mScroll / getHorizontalFadingEdgeLength(); 6888 } else { 6889 return 0.0f; 6890 } 6891 } else if (getLineCount() == 1) { 6892 switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 6893 case Gravity.LEFT: 6894 return 0.0f; 6895 case Gravity.RIGHT: 6896 return (mLayout.getLineRight(0) - (mRight - mLeft) - 6897 getCompoundPaddingLeft() - getCompoundPaddingRight() - 6898 mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength(); 6899 case Gravity.CENTER_HORIZONTAL: 6900 return 0.0f; 6901 } 6902 } 6903 } 6904 return super.getLeftFadingEdgeStrength(); 6905 } 6906 6907 @Override 6908 protected float getRightFadingEdgeStrength() { 6909 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { 6910 if (mMarquee != null && !mMarquee.isStopped()) { 6911 final Marquee marquee = mMarquee; 6912 return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength(); 6913 } else if (getLineCount() == 1) { 6914 switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 6915 case Gravity.LEFT: 6916 final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() - 6917 getCompoundPaddingRight(); 6918 final float lineWidth = mLayout.getLineWidth(0); 6919 return (lineWidth - textWidth) / getHorizontalFadingEdgeLength(); 6920 case Gravity.RIGHT: 6921 return 0.0f; 6922 case Gravity.CENTER_HORIZONTAL: 6923 return (mLayout.getLineWidth(0) - ((mRight - mLeft) - 6924 getCompoundPaddingLeft() - getCompoundPaddingRight())) / 6925 getHorizontalFadingEdgeLength(); 6926 } 6927 } 6928 } 6929 return super.getRightFadingEdgeStrength(); 6930 } 6931 6932 @Override 6933 protected int computeHorizontalScrollRange() { 6934 if (mLayout != null) { 6935 return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ? 6936 (int) mLayout.getLineWidth(0) : mLayout.getWidth(); 6937 } 6938 6939 return super.computeHorizontalScrollRange(); 6940 } 6941 6942 @Override 6943 protected int computeVerticalScrollRange() { 6944 if (mLayout != null) 6945 return mLayout.getHeight(); 6946 6947 return super.computeVerticalScrollRange(); 6948 } 6949 6950 @Override 6951 protected int computeVerticalScrollExtent() { 6952 return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); 6953 } 6954 6955 public enum BufferType { 6956 NORMAL, SPANNABLE, EDITABLE, 6957 } 6958 6959 /** 6960 * Returns the TextView_textColor attribute from the 6961 * Resources.StyledAttributes, if set, or the TextAppearance_textColor 6962 * from the TextView_textAppearance attribute, if TextView_textColor 6963 * was not set directly. 6964 */ 6965 public static ColorStateList getTextColors(Context context, TypedArray attrs) { 6966 ColorStateList colors; 6967 colors = attrs.getColorStateList(com.android.internal.R.styleable. 6968 TextView_textColor); 6969 6970 if (colors == null) { 6971 int ap = attrs.getResourceId(com.android.internal.R.styleable. 6972 TextView_textAppearance, -1); 6973 if (ap != -1) { 6974 TypedArray appearance; 6975 appearance = context.obtainStyledAttributes(ap, 6976 com.android.internal.R.styleable.TextAppearance); 6977 colors = appearance.getColorStateList(com.android.internal.R.styleable. 6978 TextAppearance_textColor); 6979 appearance.recycle(); 6980 } 6981 } 6982 6983 return colors; 6984 } 6985 6986 /** 6987 * Returns the default color from the TextView_textColor attribute 6988 * from the AttributeSet, if set, or the default color from the 6989 * TextAppearance_textColor from the TextView_textAppearance attribute, 6990 * if TextView_textColor was not set directly. 6991 */ 6992 public static int getTextColor(Context context, 6993 TypedArray attrs, 6994 int def) { 6995 ColorStateList colors = getTextColors(context, attrs); 6996 6997 if (colors == null) { 6998 return def; 6999 } else { 7000 return colors.getDefaultColor(); 7001 } 7002 } 7003 7004 @Override 7005 public boolean onKeyShortcut(int keyCode, KeyEvent event) { 7006 switch (keyCode) { 7007 case KeyEvent.KEYCODE_A: 7008 if (canSelectAll()) { 7009 return onTextContextMenuItem(ID_SELECT_ALL); 7010 } 7011 7012 break; 7013 7014 case KeyEvent.KEYCODE_X: 7015 if (canCut()) { 7016 return onTextContextMenuItem(ID_CUT); 7017 } 7018 7019 break; 7020 7021 case KeyEvent.KEYCODE_C: 7022 if (canCopy()) { 7023 return onTextContextMenuItem(ID_COPY); 7024 } 7025 7026 break; 7027 7028 case KeyEvent.KEYCODE_V: 7029 if (canPaste()) { 7030 return onTextContextMenuItem(ID_PASTE); 7031 } 7032 7033 break; 7034 } 7035 7036 return super.onKeyShortcut(keyCode, event); 7037 } 7038 7039 private boolean canSelectAll() { 7040 return canSelectText() && mText.length() != 0; 7041 } 7042 7043 private boolean canSelectText() { 7044 // prepareCursorController() relies on this method. 7045 // If you change this condition, make sure prepareCursorController is called anywhere 7046 // the value of this condition might be changed. 7047 return (mText instanceof Spannable && 7048 mMovement != null && 7049 mMovement.canSelectArbitrarily()); 7050 } 7051 7052 private boolean canCut() { 7053 if (mTransformation instanceof PasswordTransformationMethod) { 7054 return false; 7055 } 7056 7057 if (mText.length() > 0 && getSelectionStart() >= 0) { 7058 if (mText instanceof Editable && mInput != null) { 7059 return true; 7060 } 7061 } 7062 7063 return false; 7064 } 7065 7066 private boolean canCopy() { 7067 if (mTransformation instanceof PasswordTransformationMethod) { 7068 return false; 7069 } 7070 7071 if (mText.length() > 0 && getSelectionStart() >= 0) { 7072 return true; 7073 } 7074 7075 return false; 7076 } 7077 7078 private boolean canPaste() { 7079 return (mText instanceof Editable && 7080 mInput != null && 7081 getSelectionStart() >= 0 && 7082 getSelectionEnd() >= 0 && 7083 ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)). 7084 hasPrimaryClip()); 7085 } 7086 7087 /** 7088 * Returns the offsets delimiting the 'word' located at position offset. 7089 * 7090 * @param offset An offset in the text. 7091 * @return The offsets for the start and end of the word located at <code>offset</code>. 7092 * The two ints offsets are packed in a long, with the starting offset shifted by 32 bits. 7093 * Returns a negative value if no valid word was found. 7094 */ 7095 private long getWordLimitsAt(int offset) { 7096 /* 7097 * Quick return if the input type is one where adding words 7098 * to the dictionary doesn't make any sense. 7099 */ 7100 int klass = mInputType & InputType.TYPE_MASK_CLASS; 7101 if (klass == InputType.TYPE_CLASS_NUMBER || 7102 klass == InputType.TYPE_CLASS_PHONE || 7103 klass == InputType.TYPE_CLASS_DATETIME) { 7104 return -1; 7105 } 7106 7107 int variation = mInputType & InputType.TYPE_MASK_VARIATION; 7108 if (variation == InputType.TYPE_TEXT_VARIATION_URI || 7109 variation == InputType.TYPE_TEXT_VARIATION_PASSWORD || 7110 variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD || 7111 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || 7112 variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 7113 return -1; 7114 } 7115 7116 int end = offset; 7117 7118 if (end < 0) { 7119 return -1; 7120 } 7121 7122 int start = end; 7123 int len = mText.length(); 7124 7125 for (; start > 0; start--) { 7126 char c = mTransformed.charAt(start - 1); 7127 int type = Character.getType(c); 7128 7129 if (c != '\'' && 7130 type != Character.UPPERCASE_LETTER && 7131 type != Character.LOWERCASE_LETTER && 7132 type != Character.TITLECASE_LETTER && 7133 type != Character.MODIFIER_LETTER && 7134 type != Character.DECIMAL_DIGIT_NUMBER) { 7135 break; 7136 } 7137 } 7138 7139 for (; end < len; end++) { 7140 char c = mTransformed.charAt(end); 7141 int type = Character.getType(c); 7142 7143 if (c != '\'' && 7144 type != Character.UPPERCASE_LETTER && 7145 type != Character.LOWERCASE_LETTER && 7146 type != Character.TITLECASE_LETTER && 7147 type != Character.MODIFIER_LETTER && 7148 type != Character.DECIMAL_DIGIT_NUMBER) { 7149 break; 7150 } 7151 } 7152 7153 if (start == end) { 7154 return -1; 7155 } 7156 7157 if (end - start > 48) { 7158 return -1; 7159 } 7160 7161 boolean hasLetter = false; 7162 for (int i = start; i < end; i++) { 7163 if (Character.isLetter(mTransformed.charAt(i))) { 7164 hasLetter = true; 7165 break; 7166 } 7167 } 7168 7169 if (!hasLetter) { 7170 return -1; 7171 } 7172 7173 // Two ints packed in a long 7174 return (((long) start) << 32) | end; 7175 } 7176 7177 private void selectCurrentWord() { 7178 // In case selection mode is started after an orientation change or after a select all, 7179 // use the current selection instead of creating one 7180 if (hasSelection()) { 7181 return; 7182 } 7183 7184 int selectionStart, selectionEnd; 7185 7186 // selectionModifierCursorController is not null at that point 7187 SelectionModifierCursorController selectionModifierCursorController = 7188 ((SelectionModifierCursorController) mSelectionModifierCursorController); 7189 int minOffset = selectionModifierCursorController.getMinTouchOffset(); 7190 int maxOffset = selectionModifierCursorController.getMaxTouchOffset(); 7191 7192 long wordLimits = getWordLimitsAt(minOffset); 7193 if (wordLimits >= 0) { 7194 selectionStart = (int) (wordLimits >>> 32); 7195 } else { 7196 selectionStart = Math.max(minOffset - 5, 0); 7197 } 7198 7199 wordLimits = getWordLimitsAt(maxOffset); 7200 if (wordLimits >= 0) { 7201 selectionEnd = (int) (wordLimits & 0x00000000FFFFFFFFL); 7202 } else { 7203 selectionEnd = Math.min(maxOffset + 5, mText.length()); 7204 } 7205 7206 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); 7207 } 7208 7209 @Override 7210 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 7211 if (!isShown()) { 7212 return false; 7213 } 7214 7215 final boolean isPassword = isPasswordInputType(mInputType); 7216 7217 if (!isPassword) { 7218 CharSequence text = getText(); 7219 if (TextUtils.isEmpty(text)) { 7220 text = getHint(); 7221 } 7222 if (!TextUtils.isEmpty(text)) { 7223 if (text.length() > AccessibilityEvent.MAX_TEXT_LENGTH) { 7224 text = text.subSequence(0, AccessibilityEvent.MAX_TEXT_LENGTH + 1); 7225 } 7226 event.getText().add(text); 7227 } 7228 } else { 7229 event.setPassword(isPassword); 7230 } 7231 return false; 7232 } 7233 7234 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, 7235 int fromIndex, int removedCount, int addedCount) { 7236 AccessibilityEvent event = 7237 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 7238 event.setFromIndex(fromIndex); 7239 event.setRemovedCount(removedCount); 7240 event.setAddedCount(addedCount); 7241 event.setBeforeText(beforeText); 7242 sendAccessibilityEventUnchecked(event); 7243 } 7244 7245 @Override 7246 protected void onCreateContextMenu(ContextMenu menu) { 7247 super.onCreateContextMenu(menu); 7248 boolean added = false; 7249 7250 MenuHandler handler = new MenuHandler(); 7251 7252 if (mText instanceof Spanned) { 7253 int selStart = getSelectionStart(); 7254 int selEnd = getSelectionEnd(); 7255 7256 int min = Math.min(selStart, selEnd); 7257 int max = Math.max(selStart, selEnd); 7258 7259 URLSpan[] urls = ((Spanned) mText).getSpans(min, max, 7260 URLSpan.class); 7261 if (urls.length == 1) { 7262 menu.add(0, ID_COPY_URL, 0, 7263 com.android.internal.R.string.copyUrl). 7264 setOnMenuItemClickListener(handler); 7265 7266 added = true; 7267 } 7268 } 7269 7270 // The context menu is not empty, which will prevent the selection mode from starting. 7271 // Add a entry to start it in the context menu. 7272 // TODO Does not handle the case where a subclass does not call super.thisMethod or 7273 // populates the menu AFTER this call. 7274 if (menu.size() > 0) { 7275 menu.add(0, ID_SELECTION_MODE, 0, com.android.internal.R.string.selectTextMode). 7276 setOnMenuItemClickListener(handler); 7277 added = true; 7278 } 7279 7280 if (added) { 7281 menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle); 7282 } 7283 } 7284 7285 /** 7286 * Returns whether this text view is a current input method target. The 7287 * default implementation just checks with {@link InputMethodManager}. 7288 */ 7289 public boolean isInputMethodTarget() { 7290 InputMethodManager imm = InputMethodManager.peekInstance(); 7291 return imm != null && imm.isActive(this); 7292 } 7293 7294 // Selection context mode 7295 private static final int ID_SELECT_ALL = android.R.id.selectAll; 7296 private static final int ID_CUT = android.R.id.cut; 7297 private static final int ID_COPY = android.R.id.copy; 7298 private static final int ID_PASTE = android.R.id.paste; 7299 // Context menu entries 7300 private static final int ID_COPY_URL = android.R.id.copyUrl; 7301 private static final int ID_SELECTION_MODE = android.R.id.selectTextMode; 7302 7303 private class MenuHandler implements MenuItem.OnMenuItemClickListener { 7304 public boolean onMenuItemClick(MenuItem item) { 7305 return onTextContextMenuItem(item.getItemId()); 7306 } 7307 } 7308 7309 /** 7310 * Called when a context menu option for the text view is selected. Currently 7311 * this will be {@link android.R.id#copyUrl} or {@link android.R.id#selectTextMode}. 7312 */ 7313 public boolean onTextContextMenuItem(int id) { 7314 int min = 0; 7315 int max = mText.length(); 7316 7317 if (isFocused()) { 7318 int selStart = getSelectionStart(); 7319 int selEnd = getSelectionEnd(); 7320 min = Math.max(0, Math.min(selStart, selEnd)); 7321 max = Math.max(0, Math.max(selStart, selEnd)); 7322 } 7323 7324 ClipboardManager clipboard = (ClipboardManager)getContext() 7325 .getSystemService(Context.CLIPBOARD_SERVICE); 7326 7327 switch (id) { 7328 case ID_COPY_URL: 7329 MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); 7330 7331 URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class); 7332 if (urls.length >= 1) { 7333 ClippedData clip = null; 7334 for (int i=0; i<urls.length; i++) { 7335 Uri uri = Uri.parse(urls[0].getURL()); 7336 ClippedData.Item item = new ClippedData.Item(uri); 7337 if (clip == null) { 7338 clip = new ClippedData(null, null, item); 7339 } else { 7340 clip.addItem(item); 7341 } 7342 } 7343 clipboard.setPrimaryClip(clip); 7344 } 7345 return true; 7346 7347 case ID_SELECTION_MODE: 7348 startSelectionActionMode(); 7349 return true; 7350 } 7351 7352 return false; 7353 } 7354 7355 @Override 7356 public boolean performLongClick() { 7357 if (super.performLongClick()) { 7358 mEatTouchRelease = true; 7359 return true; 7360 } 7361 7362 if (startSelectionActionMode()) { 7363 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 7364 mEatTouchRelease = true; 7365 return true; 7366 } 7367 7368 return false; 7369 } 7370 7371 private boolean touchPositionIsInSelection() { 7372 int selectionStart = getSelectionStart(); 7373 int selectionEnd = getSelectionEnd(); 7374 7375 if (selectionStart == selectionEnd) { 7376 return false; 7377 } 7378 7379 if (selectionStart > selectionEnd) { 7380 int tmp = selectionStart; 7381 selectionStart = selectionEnd; 7382 selectionEnd = tmp; 7383 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); 7384 } 7385 7386 SelectionModifierCursorController selectionModifierCursorController = 7387 ((SelectionModifierCursorController) mSelectionModifierCursorController); 7388 int minOffset = selectionModifierCursorController.getMinTouchOffset(); 7389 int maxOffset = selectionModifierCursorController.getMaxTouchOffset(); 7390 7391 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); 7392 } 7393 7394 /** 7395 * Provides the callback used to start a selection action mode. 7396 * 7397 * @return A callback instance that will be used to start selection mode, or null if selection 7398 * mode is not available. 7399 */ 7400 private ActionMode.Callback getActionModeCallback() { 7401 // Long press in the current selection. 7402 // Should initiate a drag. Return false, to rely on context menu for now. 7403 if (canSelectText() && !touchPositionIsInSelection()) { 7404 return new SelectionActionModeCallback(); 7405 } 7406 return null; 7407 } 7408 7409 /** 7410 * 7411 * @return true if the selection mode was actually started. 7412 */ 7413 private boolean startSelectionActionMode() { 7414 if (mSelectionActionMode != null) { 7415 // Selection action mode is already started 7416 return false; 7417 } 7418 7419 ActionMode.Callback actionModeCallback = getActionModeCallback(); 7420 if (actionModeCallback != null) { 7421 mSelectionActionMode = startActionMode(actionModeCallback); 7422 return mSelectionActionMode != null; 7423 } 7424 7425 return false; 7426 } 7427 7428 /** 7429 * Same as {@link #stopSelectionActionMode()}, except that there is no cursor controller 7430 * fade out animation. Needed since the drawable and their alpha values are shared by all 7431 * TextViews. Switching from one TextView to another would fade the cursor controllers in the 7432 * new one otherwise. 7433 */ 7434 private void terminateSelectionActionMode() { 7435 stopSelectionActionMode(); 7436 if (mSelectionModifierCursorController != null) { 7437 SelectionModifierCursorController selectionModifierCursorController = 7438 (SelectionModifierCursorController) mSelectionModifierCursorController; 7439 selectionModifierCursorController.cancelFadeOutAnimation(); 7440 } 7441 } 7442 7443 private void stopSelectionActionMode() { 7444 if (mSelectionActionMode != null) { 7445 mSelectionActionMode.finish(); 7446 } 7447 } 7448 7449 private class SelectionActionModeCallback implements ActionMode.Callback { 7450 7451 @Override 7452 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 7453 if (mSelectionModifierCursorController == null) { 7454 Log.w(LOG_TAG, "TextView has no selection controller. Action mode cancelled."); 7455 return false; 7456 } 7457 7458 if (!requestFocus()) { 7459 return false; 7460 } 7461 7462 mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle)); 7463 mode.setSubtitle(null); 7464 7465 selectCurrentWord(); 7466 7467 boolean atLeastOne = false; 7468 7469 if (canSelectAll()) { 7470 menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). 7471 setIcon(com.android.internal.R.drawable.ic_menu_chat_dashboard). 7472 setAlphabeticShortcut('a'); 7473 atLeastOne = true; 7474 } 7475 7476 if (canCut()) { 7477 menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut). 7478 setIcon(com.android.internal.R.drawable.ic_menu_compose). 7479 setAlphabeticShortcut('x'); 7480 atLeastOne = true; 7481 } 7482 7483 if (canCopy()) { 7484 menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy). 7485 setIcon(com.android.internal.R.drawable.ic_menu_attachment). 7486 setAlphabeticShortcut('c'); 7487 atLeastOne = true; 7488 } 7489 7490 if (canPaste()) { 7491 menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). 7492 setIcon(com.android.internal.R.drawable.ic_menu_camera). 7493 setAlphabeticShortcut('v'); 7494 atLeastOne = true; 7495 } 7496 7497 if (atLeastOne) { 7498 mSelectionModifierCursorController.show(); 7499 return true; 7500 } else { 7501 return false; 7502 } 7503 } 7504 7505 @Override 7506 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 7507 return true; 7508 } 7509 7510 @Override 7511 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 7512 final int itemId = item.getItemId(); 7513 7514 if (itemId == ID_SELECT_ALL) { 7515 Selection.setSelection((Spannable) mText, 0, mText.length()); 7516 // Update controller positions after selection change. 7517 if (mSelectionModifierCursorController != null) { 7518 mSelectionModifierCursorController.show(); 7519 } 7520 return true; 7521 } 7522 7523 ClipboardManager clipboard = (ClipboardManager) getContext(). 7524 getSystemService(Context.CLIPBOARD_SERVICE); 7525 7526 int min = 0; 7527 int max = mText.length(); 7528 7529 if (isFocused()) { 7530 final int selStart = getSelectionStart(); 7531 final int selEnd = getSelectionEnd(); 7532 7533 min = Math.max(0, Math.min(selStart, selEnd)); 7534 max = Math.max(0, Math.max(selStart, selEnd)); 7535 } 7536 7537 switch (item.getItemId()) { 7538 case ID_PASTE: 7539 ClippedData clip = clipboard.getPrimaryClip(); 7540 if (clip != null) { 7541 boolean didfirst = false; 7542 for (int i=0; i<clip.getItemCount(); i++) { 7543 CharSequence paste = clip.getItem(i).coerceToText(getContext()); 7544 if (paste != null) { 7545 if (!didfirst) { 7546 Selection.setSelection((Spannable) mText, max); 7547 ((Editable) mText).replace(min, max, paste); 7548 } else { 7549 ((Editable) mText).insert(getSelectionEnd(), "\n"); 7550 ((Editable) mText).insert(getSelectionEnd(), paste); 7551 } 7552 } 7553 } 7554 stopSelectionActionMode(); 7555 } 7556 7557 return true; 7558 7559 case ID_CUT: 7560 clipboard.setPrimaryClip(new ClippedData(null, null, 7561 new ClippedData.Item(mTransformed.subSequence(min, max)))); 7562 ((Editable) mText).delete(min, max); 7563 stopSelectionActionMode(); 7564 return true; 7565 7566 case ID_COPY: 7567 clipboard.setPrimaryClip(new ClippedData(null, null, 7568 new ClippedData.Item(mTransformed.subSequence(min, max)))); 7569 stopSelectionActionMode(); 7570 return true; 7571 } 7572 7573 return false; 7574 } 7575 7576 @Override 7577 public void onDestroyActionMode(ActionMode mode) { 7578 Selection.setSelection((Spannable) mText, getSelectionStart()); 7579 if (mSelectionModifierCursorController != null) { 7580 mSelectionModifierCursorController.hide(); 7581 } 7582 mSelectionActionMode = null; 7583 } 7584 } 7585 7586 /** 7587 * A CursorController instance can be used to control a cursor in the text. 7588 * 7589 * It can be passed to an {@link ArrowKeyMovementMethod} which can intercepts events 7590 * and send them to this object instead of the cursor. 7591 */ 7592 public interface CursorController { 7593 /* Cursor fade-out animation duration, in milliseconds. */ 7594 static final int FADE_OUT_DURATION = 400; 7595 7596 /** 7597 * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. 7598 * See also {@link #hide()}. 7599 */ 7600 public void show(); 7601 7602 /** 7603 * Hide the cursor controller from screen. 7604 * See also {@link #show()}. 7605 */ 7606 public void hide(); 7607 7608 /** 7609 * Update the controller's position. 7610 */ 7611 public void updatePosition(int x, int y); 7612 7613 /** 7614 * The controller and the cursor's positions can be link by a fixed offset, 7615 * computed when the controller is touched, and then maintained as it moves 7616 * @return Horizontal offset between the controller and the cursor. 7617 */ 7618 public float getOffsetX(); 7619 7620 /** 7621 * @return Vertical offset between the controller and the cursor. 7622 */ 7623 public float getOffsetY(); 7624 7625 /** 7626 * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller 7627 * a chance to become active and/or visible. 7628 * @param event The touch event 7629 */ 7630 public void onTouchEvent(MotionEvent event); 7631 7632 /** 7633 * Draws a visual representation of the controller on the canvas. 7634 * 7635 * Called at the end of {@link #draw(Canvas)}, in the content coordinates system. 7636 * @param canvas The Canvas used by this TextView. 7637 */ 7638 public void draw(Canvas canvas); 7639 } 7640 7641 private class Handle { 7642 Drawable mDrawable; 7643 // Vertical extension of the touch region 7644 int mTopExtension, mBottomExtension; 7645 // Position of the virtual finger position on screen 7646 int mHotSpotVerticalPosition; 7647 7648 Handle(Drawable drawable) { 7649 mDrawable = drawable; 7650 } 7651 7652 void positionAtCursor(final int offset, boolean bottom) { 7653 final int drawableWidth = mDrawable.getIntrinsicWidth(); 7654 final int drawableHeight = mDrawable.getIntrinsicHeight(); 7655 final int line = mLayout.getLineForOffset(offset); 7656 final int lineTop = mLayout.getLineTop(line); 7657 final int lineBottom = mLayout.getLineBottom(line); 7658 7659 mHotSpotVerticalPosition = lineTop; 7660 7661 final Rect bounds = sCursorControllerTempRect; 7662 bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - drawableWidth / 2.0); 7663 bounds.top = (bottom ? lineBottom : lineTop) - drawableHeight / 2; 7664 7665 mTopExtension = bottom ? 0 : drawableHeight / 2; 7666 mBottomExtension = drawableHeight; 7667 7668 // Extend touch region up when editing the last line of text (or a single line) so that 7669 // it is easier to grab. 7670 if (line == mLayout.getLineCount() - 1) { 7671 mTopExtension = (lineBottom - lineTop) - drawableHeight / 2; 7672 } 7673 7674 bounds.right = bounds.left + drawableWidth; 7675 bounds.bottom = bounds.top + drawableHeight; 7676 7677 int boundTopBefore = bounds.top; 7678 convertFromViewportToContentCoordinates(bounds); 7679 mHotSpotVerticalPosition += bounds.top - boundTopBefore; 7680 mDrawable.setBounds(bounds); 7681 postInvalidate(); 7682 } 7683 7684 boolean hasFingerOn(float x, float y) { 7685 // Simulate a 'fat finger' to ease grabbing of the controller. 7686 // Expands according to controller image size instead of using dip distance. 7687 // Assumes controller imager has a sensible size, proportionnal to screen density. 7688 final int drawableWidth = mDrawable.getIntrinsicWidth(); 7689 final Rect fingerRect = sCursorControllerTempRect; 7690 fingerRect.set((int) (x - drawableWidth / 2.0), 7691 (int) (y - mBottomExtension), 7692 (int) (x + drawableWidth / 2.0), 7693 (int) (y + mTopExtension)); 7694 return Rect.intersects(mDrawable.getBounds(), fingerRect); 7695 } 7696 7697 void postInvalidate() { 7698 final Rect bounds = mDrawable.getBounds(); 7699 TextView.this.postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom); 7700 } 7701 7702 void postInvalidateDelayed(long delay) { 7703 final Rect bounds = mDrawable.getBounds(); 7704 TextView.this.postInvalidateDelayed(delay, bounds.left, bounds.top, 7705 bounds.right, bounds.bottom); 7706 } 7707 } 7708 7709 class InsertionPointCursorController implements CursorController { 7710 private static final int DELAY_BEFORE_FADE_OUT = 2100; 7711 7712 // Whether or not the cursor control is currently visible 7713 private boolean mIsVisible = false; 7714 // Starting time of the fade timer 7715 private long mFadeOutTimerStart; 7716 // The cursor controller image 7717 private final Handle mHandle; 7718 // Used to detect a tap (vs drag) on the controller 7719 private long mOnDownTimerStart; 7720 // Offset between finger hot point on cursor controller and actual cursor 7721 private float mOffsetX, mOffsetY; 7722 7723 InsertionPointCursorController() { 7724 Resources res = mContext.getResources(); 7725 mHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); 7726 } 7727 7728 public void show() { 7729 updateDrawablePosition(); 7730 // Has to be done after updateDrawablePosition, so that previous position invalidate 7731 // in only done if necessary. 7732 mIsVisible = true; 7733 } 7734 7735 public void hide() { 7736 if (mIsVisible) { 7737 long time = System.currentTimeMillis(); 7738 // Start fading out, only if not already in progress 7739 if (time - mFadeOutTimerStart < DELAY_BEFORE_FADE_OUT) { 7740 mFadeOutTimerStart = time - DELAY_BEFORE_FADE_OUT; 7741 mHandle.postInvalidate(); 7742 } 7743 } 7744 } 7745 7746 public void draw(Canvas canvas) { 7747 if (mIsVisible) { 7748 int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart); 7749 if (time <= DELAY_BEFORE_FADE_OUT) { 7750 mHandle.postInvalidateDelayed(DELAY_BEFORE_FADE_OUT - time); 7751 } else { 7752 time -= DELAY_BEFORE_FADE_OUT; 7753 if (time <= FADE_OUT_DURATION) { 7754 final int alpha = (int) 7755 ((255.0 * (FADE_OUT_DURATION - time)) / FADE_OUT_DURATION); 7756 mHandle.mDrawable.setAlpha(alpha); 7757 mHandle.postInvalidateDelayed(30); 7758 } else { 7759 mHandle.mDrawable.setAlpha(0); 7760 mIsVisible = false; 7761 } 7762 } 7763 mHandle.mDrawable.draw(canvas); 7764 } 7765 } 7766 7767 public void updatePosition(int x, int y) { 7768 final int previousOffset = getSelectionStart(); 7769 int offset = getHysteresisOffset(x, y, previousOffset); 7770 7771 if (offset != previousOffset) { 7772 Selection.setSelection((Spannable) mText, offset); 7773 updateDrawablePosition(); 7774 } 7775 } 7776 7777 private void updateDrawablePosition() { 7778 if (mIsVisible) { 7779 // Clear previous cursor controller before bounds are updated 7780 mHandle.postInvalidate(); 7781 } 7782 7783 final int offset = getSelectionStart(); 7784 7785 if (offset < 0) { 7786 // Should never happen, safety check. 7787 Log.w(LOG_TAG, "Update cursor controller position called with no cursor"); 7788 mIsVisible = false; 7789 return; 7790 } 7791 7792 mHandle.positionAtCursor(offset, true); 7793 7794 mFadeOutTimerStart = System.currentTimeMillis(); 7795 mHandle.mDrawable.setAlpha(255); 7796 } 7797 7798 public void onTouchEvent(MotionEvent event) { 7799 if (isFocused() && isTextEditable() && mIsVisible) { 7800 switch (event.getActionMasked()) { 7801 case MotionEvent.ACTION_DOWN : { 7802 final float x = event.getX(); 7803 final float y = event.getY(); 7804 7805 if (mHandle.hasFingerOn(x, y)) { 7806 show(); 7807 7808 if (mMovement instanceof ArrowKeyMovementMethod) { 7809 ((ArrowKeyMovementMethod)mMovement).setCursorController(this); 7810 } 7811 7812 if (mParent != null) { 7813 // Prevent possible scrollView parent from scrolling, so that 7814 // we can use auto-scrolling. 7815 mParent.requestDisallowInterceptTouchEvent(true); 7816 } 7817 7818 final Rect bounds = mHandle.mDrawable.getBounds(); 7819 mOffsetX = (bounds.left + bounds.right) / 2.0f - x; 7820 mOffsetY = mHandle.mHotSpotVerticalPosition - y; 7821 7822 mOnDownTimerStart = event.getEventTime(); 7823 } 7824 break; 7825 } 7826 7827 case MotionEvent.ACTION_UP : { 7828 int time = (int) (event.getEventTime() - mOnDownTimerStart); 7829 7830 if (time <= ViewConfiguration.getTapTimeout()) { 7831 // A tap on the controller (not a drag) will move the cursor 7832 int offset = getOffset((int) event.getX(), (int) event.getY()); 7833 Selection.setSelection((Spannable) mText, offset); 7834 7835 // Modified by cancelLongPress and prevents the cursor from changing 7836 mScrolled = false; 7837 } 7838 break; 7839 } 7840 } 7841 } 7842 } 7843 7844 public float getOffsetX() { 7845 return mOffsetX; 7846 } 7847 7848 public float getOffsetY() { 7849 return mOffsetY; 7850 } 7851 } 7852 7853 class SelectionModifierCursorController implements CursorController { 7854 // Whether or not the selection controls are currently visible 7855 private boolean mIsVisible = false; 7856 // Whether that start or the end of selection controller is dragged 7857 private boolean mStartIsDragged = false; 7858 // Starting time of the fade timer 7859 private long mFadeOutTimerStart; 7860 // The cursor controller images 7861 private final Handle mStartHandle, mEndHandle; 7862 // Offset between finger hot point on active cursor controller and actual cursor 7863 private float mOffsetX, mOffsetY; 7864 // The offsets of that last touch down event. Remembered to start selection there. 7865 private int mMinTouchOffset, mMaxTouchOffset; 7866 7867 SelectionModifierCursorController() { 7868 Resources res = mContext.getResources(); 7869 mStartHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); 7870 mEndHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); 7871 } 7872 7873 public void show() { 7874 updateDrawablesPositions(); 7875 // Has to be done after updateDrawablePositions, so that previous position invalidate 7876 // in only done if necessary. 7877 mIsVisible = true; 7878 mFadeOutTimerStart = -1; 7879 hideInsertionPointCursorController(); 7880 } 7881 7882 public void hide() { 7883 if (mIsVisible && (mFadeOutTimerStart < 0)) { 7884 mFadeOutTimerStart = System.currentTimeMillis(); 7885 mStartHandle.postInvalidate(); 7886 mEndHandle.postInvalidate(); 7887 } 7888 } 7889 7890 public void cancelFadeOutAnimation() { 7891 mIsVisible = false; 7892 mStartHandle.postInvalidate(); 7893 mEndHandle.postInvalidate(); 7894 } 7895 7896 public void draw(Canvas canvas) { 7897 if (mIsVisible) { 7898 if (mFadeOutTimerStart >= 0) { 7899 int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart); 7900 if (time <= FADE_OUT_DURATION) { 7901 final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION; 7902 mStartHandle.mDrawable.setAlpha(alpha); 7903 mEndHandle.mDrawable.setAlpha(alpha); 7904 mStartHandle.postInvalidateDelayed(30); 7905 mEndHandle.postInvalidateDelayed(30); 7906 } else { 7907 mStartHandle.mDrawable.setAlpha(0); 7908 mEndHandle.mDrawable.setAlpha(0); 7909 mIsVisible = false; 7910 } 7911 } 7912 mStartHandle.mDrawable.draw(canvas); 7913 mEndHandle.mDrawable.draw(canvas); 7914 } 7915 } 7916 7917 public void updatePosition(int x, int y) { 7918 int selectionStart = getSelectionStart(); 7919 int selectionEnd = getSelectionEnd(); 7920 7921 final int previousOffset = mStartIsDragged ? selectionStart : selectionEnd; 7922 int offset = getHysteresisOffset(x, y, previousOffset); 7923 7924 // Handle the case where start and end are swapped, making sure start <= end 7925 if (mStartIsDragged) { 7926 if (offset <= selectionEnd) { 7927 if (selectionStart == offset) { 7928 return; // no change, no need to redraw; 7929 } 7930 selectionStart = offset; 7931 } else { 7932 selectionStart = selectionEnd; 7933 selectionEnd = offset; 7934 mStartIsDragged = false; 7935 } 7936 } else { 7937 if (offset >= selectionStart) { 7938 if (selectionEnd == offset) { 7939 return; // no change, no need to redraw; 7940 } 7941 selectionEnd = offset; 7942 } else { 7943 selectionEnd = selectionStart; 7944 selectionStart = offset; 7945 mStartIsDragged = true; 7946 } 7947 } 7948 7949 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); 7950 updateDrawablesPositions(); 7951 } 7952 7953 private void updateDrawablesPositions() { 7954 if (mIsVisible) { 7955 // Clear previous cursor controller before bounds are updated 7956 mStartHandle.postInvalidate(); 7957 mEndHandle.postInvalidate(); 7958 } 7959 7960 final int selectionStart = getSelectionStart(); 7961 final int selectionEnd = getSelectionEnd(); 7962 7963 if ((selectionStart < 0) || (selectionEnd < 0)) { 7964 // Should never happen, safety check. 7965 Log.w(LOG_TAG, "Update selection controller position called with no cursor"); 7966 mIsVisible = false; 7967 return; 7968 } 7969 7970 boolean oneLineSelection = 7971 mLayout.getLineForOffset(selectionStart) == mLayout.getLineForOffset(selectionEnd); 7972 mStartHandle.positionAtCursor(selectionStart, oneLineSelection); 7973 mEndHandle.positionAtCursor(selectionEnd, true); 7974 7975 mStartHandle.mDrawable.setAlpha(255); 7976 mEndHandle.mDrawable.setAlpha(255); 7977 } 7978 7979 public void onTouchEvent(MotionEvent event) { 7980 if (isTextEditable()) { 7981 switch (event.getActionMasked()) { 7982 case MotionEvent.ACTION_DOWN: 7983 final int x = (int) event.getX(); 7984 final int y = (int) event.getY(); 7985 7986 // Remember finger down position, to be able to start selection from there 7987 mMinTouchOffset = mMaxTouchOffset = getOffset(x, y); 7988 7989 if (mIsVisible) { 7990 if (mMovement instanceof ArrowKeyMovementMethod) { 7991 boolean isOnStart = mStartHandle.hasFingerOn(x, y); 7992 boolean isOnEnd = mEndHandle.hasFingerOn(x, y); 7993 if (isOnStart || isOnEnd) { 7994 if (mParent != null) { 7995 // Prevent possible scrollView parent from scrolling, so 7996 // that we can use auto-scrolling. 7997 mParent.requestDisallowInterceptTouchEvent(true); 7998 } 7999 8000 // In case both controllers are under finger (very small 8001 // selection region), arbitrarily pick end controller. 8002 mStartIsDragged = !isOnEnd; 8003 final Handle draggedHandle = mStartIsDragged ? mStartHandle : mEndHandle; 8004 final Rect bounds = draggedHandle.mDrawable.getBounds(); 8005 mOffsetX = (bounds.left + bounds.right) / 2.0f - x; 8006 mOffsetY = draggedHandle.mHotSpotVerticalPosition - y; 8007 8008 ((ArrowKeyMovementMethod)mMovement).setCursorController(this); 8009 } 8010 } 8011 } 8012 break; 8013 8014 case MotionEvent.ACTION_POINTER_DOWN: 8015 case MotionEvent.ACTION_POINTER_UP: 8016 // Handle multi-point gestures. Keep min and max offset positions. 8017 // Only activated for devices that correctly handle multi-touch. 8018 if (mContext.getPackageManager().hasSystemFeature( 8019 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { 8020 updateMinAndMaxOffsets(event); 8021 } 8022 break; 8023 } 8024 } 8025 } 8026 8027 /** 8028 * @param event 8029 */ 8030 private void updateMinAndMaxOffsets(MotionEvent event) { 8031 int pointerCount = event.getPointerCount(); 8032 for (int index = 0; index < pointerCount; index++) { 8033 final int x = (int) event.getX(index); 8034 final int y = (int) event.getY(index); 8035 int offset = getOffset(x, y); 8036 if (offset < mMinTouchOffset) mMinTouchOffset = offset; 8037 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; 8038 } 8039 } 8040 8041 public int getMinTouchOffset() { 8042 return mMinTouchOffset; 8043 } 8044 8045 public int getMaxTouchOffset() { 8046 return mMaxTouchOffset; 8047 } 8048 8049 public float getOffsetX() { 8050 return mOffsetX; 8051 } 8052 8053 public float getOffsetY() { 8054 return mOffsetY; 8055 } 8056 8057 /** 8058 * @return true iff this controller is currently used to move the selection start. 8059 */ 8060 public boolean isSelectionStartDragged() { 8061 return mIsVisible && mStartIsDragged; 8062 } 8063 } 8064 8065 private void hideInsertionPointCursorController() { 8066 if (mInsertionPointCursorController != null) { 8067 mInsertionPointCursorController.hide(); 8068 } 8069 } 8070 8071 private void hideControllers() { 8072 hideInsertionPointCursorController(); 8073 stopSelectionActionMode(); 8074 } 8075 8076 private int getOffsetForHorizontal(int line, int x) { 8077 x -= getTotalPaddingLeft(); 8078 // Clamp the position to inside of the view. 8079 x = Math.max(0, x); 8080 x = Math.min(getWidth() - getTotalPaddingRight() - 1, x); 8081 x += getScrollX(); 8082 return getLayout().getOffsetForHorizontal(line, x); 8083 } 8084 8085 /** 8086 * Get the offset character closest to the specified absolute position. 8087 * 8088 * @param x The horizontal absolute position of a point on screen 8089 * @param y The vertical absolute position of a point on screen 8090 * @return the character offset for the character whose position is closest to the specified 8091 * position. Returns -1 if there is no layout. 8092 * 8093 * @hide 8094 */ 8095 public int getOffset(int x, int y) { 8096 if (getLayout() == null) return -1; 8097 8098 y -= getTotalPaddingTop(); 8099 // Clamp the position to inside of the view. 8100 y = Math.max(0, y); 8101 y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y); 8102 y += getScrollY(); 8103 8104 final int line = getLayout().getLineForVertical(y); 8105 final int offset = getOffsetForHorizontal(line, x); 8106 return offset; 8107 } 8108 8109 int getHysteresisOffset(int x, int y, int previousOffset) { 8110 final Layout layout = getLayout(); 8111 if (layout == null) return -1; 8112 8113 y -= getTotalPaddingTop(); 8114 // Clamp the position to inside of the view. 8115 y = Math.max(0, y); 8116 y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y); 8117 y += getScrollY(); 8118 8119 int line = getLayout().getLineForVertical(y); 8120 8121 final int previousLine = layout.getLineForOffset(previousOffset); 8122 final int previousLineTop = layout.getLineTop(previousLine); 8123 final int previousLineBottom = layout.getLineBottom(previousLine); 8124 final int hysteresisThreshold = (previousLineBottom - previousLineTop) / 2; 8125 8126 // If new line is just before or after previous line and y position is less than 8127 // hysteresisThreshold away from previous line, keep cursor on previous line. 8128 if (((line == previousLine + 1) && ((y - previousLineBottom) < hysteresisThreshold)) || 8129 ((line == previousLine - 1) && ((previousLineTop - y) < hysteresisThreshold))) { 8130 line = previousLine; 8131 } 8132 8133 return getOffsetForHorizontal(line, x); 8134 } 8135 8136 8137 @ViewDebug.ExportedProperty(category = "text") 8138 private CharSequence mText; 8139 private CharSequence mTransformed; 8140 private BufferType mBufferType = BufferType.NORMAL; 8141 8142 private int mInputType = EditorInfo.TYPE_NULL; 8143 private CharSequence mHint; 8144 private Layout mHintLayout; 8145 8146 private KeyListener mInput; 8147 8148 private MovementMethod mMovement; 8149 private TransformationMethod mTransformation; 8150 private ChangeWatcher mChangeWatcher; 8151 8152 private ArrayList<TextWatcher> mListeners = null; 8153 8154 // display attributes 8155 private final TextPaint mTextPaint; 8156 private boolean mUserSetTextScaleX; 8157 private final Paint mHighlightPaint; 8158 private int mHighlightColor = 0xCC475925; 8159 private Layout mLayout; 8160 8161 private long mShowCursor; 8162 private Blink mBlink; 8163 private boolean mCursorVisible = true; 8164 8165 // Cursor Controllers. Null when disabled. 8166 private CursorController mInsertionPointCursorController; 8167 private CursorController mSelectionModifierCursorController; 8168 private boolean mShouldStartSelectionActionMode = false; 8169 private ActionMode mSelectionActionMode; 8170 // Created once and shared by different CursorController helper methods. 8171 // Only one cursor controller is active at any time which prevent race conditions. 8172 private static Rect sCursorControllerTempRect = new Rect(); 8173 8174 private boolean mSelectAllOnFocus = false; 8175 8176 private int mGravity = Gravity.TOP | Gravity.LEFT; 8177 private boolean mHorizontallyScrolling; 8178 8179 private int mAutoLinkMask; 8180 private boolean mLinksClickable = true; 8181 8182 private float mSpacingMult = 1; 8183 private float mSpacingAdd = 0; 8184 8185 private static final int LINES = 1; 8186 private static final int EMS = LINES; 8187 private static final int PIXELS = 2; 8188 8189 private int mMaximum = Integer.MAX_VALUE; 8190 private int mMaxMode = LINES; 8191 private int mMinimum = 0; 8192 private int mMinMode = LINES; 8193 8194 private int mMaxWidth = Integer.MAX_VALUE; 8195 private int mMaxWidthMode = PIXELS; 8196 private int mMinWidth = 0; 8197 private int mMinWidthMode = PIXELS; 8198 8199 private boolean mSingleLine; 8200 private int mDesiredHeightAtMeasure = -1; 8201 private boolean mIncludePad = true; 8202 8203 // tmp primitives, so we don't alloc them on each draw 8204 private Path mHighlightPath; 8205 private boolean mHighlightPathBogus = true; 8206 private static final RectF sTempRect = new RectF(); 8207 8208 // XXX should be much larger 8209 private static final int VERY_WIDE = 16384; 8210 8211 private static final int BLINK = 500; 8212 8213 private static final int ANIMATED_SCROLL_GAP = 250; 8214 private long mLastScroll; 8215 private Scroller mScroller = null; 8216 8217 private BoringLayout.Metrics mBoring; 8218 private BoringLayout.Metrics mHintBoring; 8219 8220 private BoringLayout mSavedLayout, mSavedHintLayout; 8221 8222 private static final InputFilter[] NO_FILTERS = new InputFilter[0]; 8223 private InputFilter[] mFilters = NO_FILTERS; 8224 private static final Spanned EMPTY_SPANNED = new SpannedString(""); 8225} 8226