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