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