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