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