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