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