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