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