Editor.java revision 1957d281ea123e4925e51fa5ad22ce239ef2a07d
1/* 2 * Copyright (C) 2012 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 com.android.internal.util.ArrayUtils; 20import com.android.internal.widget.EditableInputConnection; 21 22import android.R; 23import android.content.ClipData; 24import android.content.ClipData.Item; 25import android.content.Context; 26import android.content.Intent; 27import android.content.pm.PackageManager; 28import android.content.res.TypedArray; 29import android.graphics.Canvas; 30import android.graphics.Color; 31import android.graphics.Paint; 32import android.graphics.Path; 33import android.graphics.Rect; 34import android.graphics.RectF; 35import android.graphics.drawable.Drawable; 36import android.inputmethodservice.ExtractEditText; 37import android.os.Bundle; 38import android.os.Handler; 39import android.os.SystemClock; 40import android.provider.Settings; 41import android.text.DynamicLayout; 42import android.text.Editable; 43import android.text.InputType; 44import android.text.Layout; 45import android.text.ParcelableSpan; 46import android.text.Selection; 47import android.text.SpanWatcher; 48import android.text.Spannable; 49import android.text.SpannableStringBuilder; 50import android.text.Spanned; 51import android.text.StaticLayout; 52import android.text.TextUtils; 53import android.text.method.KeyListener; 54import android.text.method.MetaKeyKeyListener; 55import android.text.method.MovementMethod; 56import android.text.method.PasswordTransformationMethod; 57import android.text.method.WordIterator; 58import android.text.style.EasyEditSpan; 59import android.text.style.SuggestionRangeSpan; 60import android.text.style.SuggestionSpan; 61import android.text.style.TextAppearanceSpan; 62import android.text.style.URLSpan; 63import android.util.DisplayMetrics; 64import android.util.Log; 65import android.view.ActionMode; 66import android.view.ActionMode.Callback; 67import android.view.DisplayList; 68import android.view.DragEvent; 69import android.view.Gravity; 70import android.view.HardwareCanvas; 71import android.view.LayoutInflater; 72import android.view.Menu; 73import android.view.MenuItem; 74import android.view.MotionEvent; 75import android.view.View; 76import android.view.View.DragShadowBuilder; 77import android.view.View.OnClickListener; 78import android.view.ViewConfiguration; 79import android.view.ViewGroup; 80import android.view.ViewGroup.LayoutParams; 81import android.view.ViewParent; 82import android.view.ViewTreeObserver; 83import android.view.WindowManager; 84import android.view.inputmethod.CorrectionInfo; 85import android.view.inputmethod.EditorInfo; 86import android.view.inputmethod.ExtractedText; 87import android.view.inputmethod.ExtractedTextRequest; 88import android.view.inputmethod.InputConnection; 89import android.view.inputmethod.InputMethodManager; 90import android.widget.AdapterView.OnItemClickListener; 91import android.widget.TextView.Drawables; 92import android.widget.TextView.OnEditorActionListener; 93 94import java.text.BreakIterator; 95import java.util.Arrays; 96import java.util.Comparator; 97import java.util.HashMap; 98 99/** 100 * Helper class used by TextView to handle editable text views. 101 * 102 * @hide 103 */ 104public class Editor { 105 private static final String TAG = "Editor"; 106 107 static final int BLINK = 500; 108 private static final float[] TEMP_POSITION = new float[2]; 109 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; 110 111 // Cursor Controllers. 112 InsertionPointCursorController mInsertionPointCursorController; 113 SelectionModifierCursorController mSelectionModifierCursorController; 114 ActionMode mSelectionActionMode; 115 boolean mInsertionControllerEnabled; 116 boolean mSelectionControllerEnabled; 117 118 // Used to highlight a word when it is corrected by the IME 119 CorrectionHighlighter mCorrectionHighlighter; 120 121 InputContentType mInputContentType; 122 InputMethodState mInputMethodState; 123 124 DisplayList[] mTextDisplayLists; 125 int mLastLayoutHeight; 126 127 boolean mFrozenWithFocus; 128 boolean mSelectionMoved; 129 boolean mTouchFocusSelected; 130 131 KeyListener mKeyListener; 132 int mInputType = EditorInfo.TYPE_NULL; 133 134 boolean mDiscardNextActionUp; 135 boolean mIgnoreActionUpEvent; 136 137 long mShowCursor; 138 Blink mBlink; 139 140 boolean mCursorVisible = true; 141 boolean mSelectAllOnFocus; 142 boolean mTextIsSelectable; 143 144 CharSequence mError; 145 boolean mErrorWasChanged; 146 ErrorPopup mErrorPopup; 147 private int mLastLayoutDirection = -1; 148 149 /** 150 * This flag is set if the TextView tries to display an error before it 151 * is attached to the window (so its position is still unknown). 152 * It causes the error to be shown later, when onAttachedToWindow() 153 * is called. 154 */ 155 boolean mShowErrorAfterAttach; 156 157 boolean mInBatchEditControllers; 158 boolean mShowSoftInputOnFocus = true; 159 boolean mPreserveDetachedSelection; 160 boolean mTemporaryDetach; 161 162 SuggestionsPopupWindow mSuggestionsPopupWindow; 163 SuggestionRangeSpan mSuggestionRangeSpan; 164 Runnable mShowSuggestionRunnable; 165 166 final Drawable[] mCursorDrawable = new Drawable[2]; 167 int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split) 168 169 private Drawable mSelectHandleLeft; 170 private Drawable mSelectHandleRight; 171 private Drawable mSelectHandleCenter; 172 173 // Global listener that detects changes in the global position of the TextView 174 private PositionListener mPositionListener; 175 176 float mLastDownPositionX, mLastDownPositionY; 177 Callback mCustomSelectionActionModeCallback; 178 179 // Set when this TextView gained focus with some text selected. Will start selection mode. 180 boolean mCreatedWithASelection; 181 182 private EasyEditSpanController mEasyEditSpanController; 183 184 WordIterator mWordIterator; 185 SpellChecker mSpellChecker; 186 187 private Rect mTempRect; 188 189 private TextView mTextView; 190 191 Editor(TextView textView) { 192 mTextView = textView; 193 } 194 195 void onAttachedToWindow() { 196 if (mShowErrorAfterAttach) { 197 showError(); 198 mShowErrorAfterAttach = false; 199 } 200 mTemporaryDetach = false; 201 202 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 203 // No need to create the controller. 204 // The get method will add the listener on controller creation. 205 if (mInsertionPointCursorController != null) { 206 observer.addOnTouchModeChangeListener(mInsertionPointCursorController); 207 } 208 if (mSelectionModifierCursorController != null) { 209 mSelectionModifierCursorController.resetTouchOffsets(); 210 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); 211 } 212 updateSpellCheckSpans(0, mTextView.getText().length(), 213 true /* create the spell checker if needed */); 214 215 if (mTextView.hasTransientState() && 216 mTextView.getSelectionStart() != mTextView.getSelectionEnd()) { 217 // Since transient state is reference counted make sure it stays matched 218 // with our own calls to it for managing selection. 219 // The action mode callback will set this back again when/if the action mode starts. 220 mTextView.setHasTransientState(false); 221 222 // We had an active selection from before, start the selection mode. 223 startSelectionActionMode(); 224 } 225 } 226 227 void onDetachedFromWindow() { 228 if (mError != null) { 229 hideError(); 230 } 231 232 if (mBlink != null) { 233 mBlink.removeCallbacks(mBlink); 234 } 235 236 if (mInsertionPointCursorController != null) { 237 mInsertionPointCursorController.onDetached(); 238 } 239 240 if (mSelectionModifierCursorController != null) { 241 mSelectionModifierCursorController.onDetached(); 242 } 243 244 if (mShowSuggestionRunnable != null) { 245 mTextView.removeCallbacks(mShowSuggestionRunnable); 246 } 247 248 invalidateTextDisplayList(); 249 250 if (mSpellChecker != null) { 251 mSpellChecker.closeSession(); 252 // Forces the creation of a new SpellChecker next time this window is created. 253 // Will handle the cases where the settings has been changed in the meantime. 254 mSpellChecker = null; 255 } 256 257 mPreserveDetachedSelection = true; 258 hideControllers(); 259 mPreserveDetachedSelection = false; 260 mTemporaryDetach = false; 261 } 262 263 private void showError() { 264 if (mTextView.getWindowToken() == null) { 265 mShowErrorAfterAttach = true; 266 return; 267 } 268 269 if (mErrorPopup == null) { 270 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext()); 271 final TextView err = (TextView) inflater.inflate( 272 com.android.internal.R.layout.textview_hint, null); 273 274 final float scale = mTextView.getResources().getDisplayMetrics().density; 275 mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f)); 276 mErrorPopup.setFocusable(false); 277 // The user is entering text, so the input method is needed. We 278 // don't want the popup to be displayed on top of it. 279 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 280 } 281 282 TextView tv = (TextView) mErrorPopup.getContentView(); 283 chooseSize(mErrorPopup, mError, tv); 284 tv.setText(mError); 285 286 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY()); 287 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor()); 288 } 289 290 public void setError(CharSequence error, Drawable icon) { 291 mError = TextUtils.stringOrSpannedString(error); 292 mErrorWasChanged = true; 293 final int layoutDirection = mTextView.getLayoutDirection(); 294 if (mLastLayoutDirection != layoutDirection) { 295 final Drawables dr = mTextView.mDrawables; 296 switch (layoutDirection) { 297 default: 298 case View.LAYOUT_DIRECTION_LTR: 299 if (dr != null) { 300 mTextView.setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, 301 dr.mDrawableBottom); 302 } else { 303 mTextView.setCompoundDrawables(null, null, icon, null); 304 } 305 break; 306 case View.LAYOUT_DIRECTION_RTL: 307 if (dr != null) { 308 mTextView.setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight, 309 dr.mDrawableBottom); 310 } else { 311 mTextView.setCompoundDrawables(icon, null, null, null); 312 } 313 break; 314 } 315 mLastLayoutDirection = layoutDirection; 316 } 317 if (mError == null) { 318 if (mErrorPopup != null) { 319 if (mErrorPopup.isShowing()) { 320 mErrorPopup.dismiss(); 321 } 322 323 mErrorPopup = null; 324 } 325 } else { 326 if (mTextView.isFocused()) { 327 showError(); 328 } 329 } 330 } 331 332 private void hideError() { 333 if (mErrorPopup != null) { 334 if (mErrorPopup.isShowing()) { 335 mErrorPopup.dismiss(); 336 } 337 } 338 339 mShowErrorAfterAttach = false; 340 } 341 342 /** 343 * Returns the Y offset to make the pointy top of the error point 344 * at the middle of the error icon. 345 */ 346 private int getErrorX() { 347 /* 348 * The "25" is the distance between the point and the right edge 349 * of the background 350 */ 351 final float scale = mTextView.getResources().getDisplayMetrics().density; 352 353 final Drawables dr = mTextView.mDrawables; 354 return mTextView.getWidth() - mErrorPopup.getWidth() - mTextView.getPaddingRight() - 355 (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f); 356 } 357 358 /** 359 * Returns the Y offset to make the pointy top of the error point 360 * at the bottom of the error icon. 361 */ 362 private int getErrorY() { 363 /* 364 * Compound, not extended, because the icon is not clipped 365 * if the text height is smaller. 366 */ 367 final int compoundPaddingTop = mTextView.getCompoundPaddingTop(); 368 int vspace = mTextView.getBottom() - mTextView.getTop() - 369 mTextView.getCompoundPaddingBottom() - compoundPaddingTop; 370 371 final Drawables dr = mTextView.mDrawables; 372 int icontop = compoundPaddingTop + 373 (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2; 374 375 /* 376 * The "2" is the distance between the point and the top edge 377 * of the background. 378 */ 379 final float scale = mTextView.getResources().getDisplayMetrics().density; 380 return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - mTextView.getHeight() - 381 (int) (2 * scale + 0.5f); 382 } 383 384 void createInputContentTypeIfNeeded() { 385 if (mInputContentType == null) { 386 mInputContentType = new InputContentType(); 387 } 388 } 389 390 void createInputMethodStateIfNeeded() { 391 if (mInputMethodState == null) { 392 mInputMethodState = new InputMethodState(); 393 } 394 } 395 396 boolean isCursorVisible() { 397 // The default value is true, even when there is no associated Editor 398 return mCursorVisible && mTextView.isTextEditable(); 399 } 400 401 void prepareCursorControllers() { 402 boolean windowSupportsHandles = false; 403 404 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams(); 405 if (params instanceof WindowManager.LayoutParams) { 406 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params; 407 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW 408 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW; 409 } 410 411 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null; 412 mInsertionControllerEnabled = enabled && isCursorVisible(); 413 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected(); 414 415 if (!mInsertionControllerEnabled) { 416 hideInsertionPointCursorController(); 417 if (mInsertionPointCursorController != null) { 418 mInsertionPointCursorController.onDetached(); 419 mInsertionPointCursorController = null; 420 } 421 } 422 423 if (!mSelectionControllerEnabled) { 424 stopSelectionActionMode(); 425 if (mSelectionModifierCursorController != null) { 426 mSelectionModifierCursorController.onDetached(); 427 mSelectionModifierCursorController = null; 428 } 429 } 430 } 431 432 private void hideInsertionPointCursorController() { 433 if (mInsertionPointCursorController != null) { 434 mInsertionPointCursorController.hide(); 435 } 436 } 437 438 /** 439 * Hides the insertion controller and stops text selection mode, hiding the selection controller 440 */ 441 void hideControllers() { 442 hideCursorControllers(); 443 hideSpanControllers(); 444 } 445 446 private void hideSpanControllers() { 447 if (mEasyEditSpanController != null) { 448 mEasyEditSpanController.hide(); 449 } 450 } 451 452 private void hideCursorControllers() { 453 if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) { 454 // Should be done before hide insertion point controller since it triggers a show of it 455 mSuggestionsPopupWindow.hide(); 456 } 457 hideInsertionPointCursorController(); 458 stopSelectionActionMode(); 459 } 460 461 /** 462 * Create new SpellCheckSpans on the modified region. 463 */ 464 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) { 465 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() && 466 !(mTextView instanceof ExtractEditText)) { 467 if (mSpellChecker == null && createSpellChecker) { 468 mSpellChecker = new SpellChecker(mTextView); 469 } 470 if (mSpellChecker != null) { 471 mSpellChecker.spellCheck(start, end); 472 } 473 } 474 } 475 476 void onScreenStateChanged(int screenState) { 477 switch (screenState) { 478 case View.SCREEN_STATE_ON: 479 resumeBlink(); 480 break; 481 case View.SCREEN_STATE_OFF: 482 suspendBlink(); 483 break; 484 } 485 } 486 487 private void suspendBlink() { 488 if (mBlink != null) { 489 mBlink.cancel(); 490 } 491 } 492 493 private void resumeBlink() { 494 if (mBlink != null) { 495 mBlink.uncancel(); 496 makeBlink(); 497 } 498 } 499 500 void adjustInputType(boolean password, boolean passwordInputType, 501 boolean webPasswordInputType, boolean numberPasswordInputType) { 502 // mInputType has been set from inputType, possibly modified by mInputMethod. 503 // Specialize mInputType to [web]password if we have a text class and the original input 504 // type was a password. 505 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { 506 if (password || passwordInputType) { 507 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) 508 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; 509 } 510 if (webPasswordInputType) { 511 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) 512 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD; 513 } 514 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) { 515 if (numberPasswordInputType) { 516 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) 517 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD; 518 } 519 } 520 } 521 522 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { 523 int wid = tv.getPaddingLeft() + tv.getPaddingRight(); 524 int ht = tv.getPaddingTop() + tv.getPaddingBottom(); 525 526 int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize( 527 com.android.internal.R.dimen.textview_error_popup_default_width); 528 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels, 529 Layout.Alignment.ALIGN_NORMAL, 1, 0, true); 530 float max = 0; 531 for (int i = 0; i < l.getLineCount(); i++) { 532 max = Math.max(max, l.getLineWidth(i)); 533 } 534 535 /* 536 * Now set the popup size to be big enough for the text plus the border capped 537 * to DEFAULT_MAX_POPUP_WIDTH 538 */ 539 pop.setWidth(wid + (int) Math.ceil(max)); 540 pop.setHeight(ht + l.getHeight()); 541 } 542 543 void setFrame() { 544 if (mErrorPopup != null) { 545 TextView tv = (TextView) mErrorPopup.getContentView(); 546 chooseSize(mErrorPopup, mError, tv); 547 mErrorPopup.update(mTextView, getErrorX(), getErrorY(), 548 mErrorPopup.getWidth(), mErrorPopup.getHeight()); 549 } 550 } 551 552 /** 553 * Unlike {@link TextView#textCanBeSelected()}, this method is based on the <i>current</i> state 554 * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have 555 * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient. 556 */ 557 private boolean canSelectText() { 558 return hasSelectionController() && mTextView.getText().length() != 0; 559 } 560 561 /** 562 * It would be better to rely on the input type for everything. A password inputType should have 563 * a password transformation. We should hence use isPasswordInputType instead of this method. 564 * 565 * We should: 566 * - Call setInputType in setKeyListener instead of changing the input type directly (which 567 * would install the correct transformation). 568 * - Refuse the installation of a non-password transformation in setTransformation if the input 569 * type is password. 570 * 571 * However, this is like this for legacy reasons and we cannot break existing apps. This method 572 * is useful since it matches what the user can see (obfuscated text or not). 573 * 574 * @return true if the current transformation method is of the password type. 575 */ 576 private boolean hasPasswordTransformationMethod() { 577 return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod; 578 } 579 580 /** 581 * Adjusts selection to the word under last touch offset. 582 * Return true if the operation was successfully performed. 583 */ 584 private boolean selectCurrentWord() { 585 if (!canSelectText()) { 586 return false; 587 } 588 589 if (hasPasswordTransformationMethod()) { 590 // Always select all on a password field. 591 // Cut/copy menu entries are not available for passwords, but being able to select all 592 // is however useful to delete or paste to replace the entire content. 593 return mTextView.selectAllText(); 594 } 595 596 int inputType = mTextView.getInputType(); 597 int klass = inputType & InputType.TYPE_MASK_CLASS; 598 int variation = inputType & InputType.TYPE_MASK_VARIATION; 599 600 // Specific text field types: select the entire text for these 601 if (klass == InputType.TYPE_CLASS_NUMBER || 602 klass == InputType.TYPE_CLASS_PHONE || 603 klass == InputType.TYPE_CLASS_DATETIME || 604 variation == InputType.TYPE_TEXT_VARIATION_URI || 605 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || 606 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || 607 variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 608 return mTextView.selectAllText(); 609 } 610 611 long lastTouchOffsets = getLastTouchOffsets(); 612 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets); 613 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets); 614 615 // Safety check in case standard touch event handling has been bypassed 616 if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false; 617 if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false; 618 619 int selectionStart, selectionEnd; 620 621 // If a URLSpan (web address, email, phone...) is found at that position, select it. 622 URLSpan[] urlSpans = ((Spanned) mTextView.getText()). 623 getSpans(minOffset, maxOffset, URLSpan.class); 624 if (urlSpans.length >= 1) { 625 URLSpan urlSpan = urlSpans[0]; 626 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan); 627 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan); 628 } else { 629 final WordIterator wordIterator = getWordIterator(); 630 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset); 631 632 selectionStart = wordIterator.getBeginning(minOffset); 633 selectionEnd = wordIterator.getEnd(maxOffset); 634 635 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE || 636 selectionStart == selectionEnd) { 637 // Possible when the word iterator does not properly handle the text's language 638 long range = getCharRange(minOffset); 639 selectionStart = TextUtils.unpackRangeStartFromLong(range); 640 selectionEnd = TextUtils.unpackRangeEndFromLong(range); 641 } 642 } 643 644 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 645 return selectionEnd > selectionStart; 646 } 647 648 void onLocaleChanged() { 649 // Will be re-created on demand in getWordIterator with the proper new locale 650 mWordIterator = null; 651 } 652 653 /** 654 * @hide 655 */ 656 public WordIterator getWordIterator() { 657 if (mWordIterator == null) { 658 mWordIterator = new WordIterator(mTextView.getTextServicesLocale()); 659 } 660 return mWordIterator; 661 } 662 663 private long getCharRange(int offset) { 664 final int textLength = mTextView.getText().length(); 665 if (offset + 1 < textLength) { 666 final char currentChar = mTextView.getText().charAt(offset); 667 final char nextChar = mTextView.getText().charAt(offset + 1); 668 if (Character.isSurrogatePair(currentChar, nextChar)) { 669 return TextUtils.packRangeInLong(offset, offset + 2); 670 } 671 } 672 if (offset < textLength) { 673 return TextUtils.packRangeInLong(offset, offset + 1); 674 } 675 if (offset - 2 >= 0) { 676 final char previousChar = mTextView.getText().charAt(offset - 1); 677 final char previousPreviousChar = mTextView.getText().charAt(offset - 2); 678 if (Character.isSurrogatePair(previousPreviousChar, previousChar)) { 679 return TextUtils.packRangeInLong(offset - 2, offset); 680 } 681 } 682 if (offset - 1 >= 0) { 683 return TextUtils.packRangeInLong(offset - 1, offset); 684 } 685 return TextUtils.packRangeInLong(offset, offset); 686 } 687 688 private boolean touchPositionIsInSelection() { 689 int selectionStart = mTextView.getSelectionStart(); 690 int selectionEnd = mTextView.getSelectionEnd(); 691 692 if (selectionStart == selectionEnd) { 693 return false; 694 } 695 696 if (selectionStart > selectionEnd) { 697 int tmp = selectionStart; 698 selectionStart = selectionEnd; 699 selectionEnd = tmp; 700 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 701 } 702 703 SelectionModifierCursorController selectionController = getSelectionController(); 704 int minOffset = selectionController.getMinTouchOffset(); 705 int maxOffset = selectionController.getMaxTouchOffset(); 706 707 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); 708 } 709 710 private PositionListener getPositionListener() { 711 if (mPositionListener == null) { 712 mPositionListener = new PositionListener(); 713 } 714 return mPositionListener; 715 } 716 717 private interface TextViewPositionListener { 718 public void updatePosition(int parentPositionX, int parentPositionY, 719 boolean parentPositionChanged, boolean parentScrolled); 720 } 721 722 private boolean isPositionVisible(int positionX, int positionY) { 723 synchronized (TEMP_POSITION) { 724 final float[] position = TEMP_POSITION; 725 position[0] = positionX; 726 position[1] = positionY; 727 View view = mTextView; 728 729 while (view != null) { 730 if (view != mTextView) { 731 // Local scroll is already taken into account in positionX/Y 732 position[0] -= view.getScrollX(); 733 position[1] -= view.getScrollY(); 734 } 735 736 if (position[0] < 0 || position[1] < 0 || 737 position[0] > view.getWidth() || position[1] > view.getHeight()) { 738 return false; 739 } 740 741 if (!view.getMatrix().isIdentity()) { 742 view.getMatrix().mapPoints(position); 743 } 744 745 position[0] += view.getLeft(); 746 position[1] += view.getTop(); 747 748 final ViewParent parent = view.getParent(); 749 if (parent instanceof View) { 750 view = (View) parent; 751 } else { 752 // We've reached the ViewRoot, stop iterating 753 view = null; 754 } 755 } 756 } 757 758 // We've been able to walk up the view hierarchy and the position was never clipped 759 return true; 760 } 761 762 private boolean isOffsetVisible(int offset) { 763 Layout layout = mTextView.getLayout(); 764 final int line = layout.getLineForOffset(offset); 765 final int lineBottom = layout.getLineBottom(line); 766 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset); 767 return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(), 768 lineBottom + mTextView.viewportToContentVerticalOffset()); 769 } 770 771 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed 772 * in the view. Returns false when the position is in the empty space of left/right of text. 773 */ 774 private boolean isPositionOnText(float x, float y) { 775 Layout layout = mTextView.getLayout(); 776 if (layout == null) return false; 777 778 final int line = mTextView.getLineAtCoordinate(y); 779 x = mTextView.convertToLocalHorizontalCoordinate(x); 780 781 if (x < layout.getLineLeft(line)) return false; 782 if (x > layout.getLineRight(line)) return false; 783 return true; 784 } 785 786 public boolean performLongClick(boolean handled) { 787 // Long press in empty space moves cursor and shows the Paste affordance if available. 788 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) && 789 mInsertionControllerEnabled) { 790 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX, 791 mLastDownPositionY); 792 stopSelectionActionMode(); 793 Selection.setSelection((Spannable) mTextView.getText(), offset); 794 getInsertionController().showWithActionPopup(); 795 handled = true; 796 } 797 798 if (!handled && mSelectionActionMode != null) { 799 if (touchPositionIsInSelection()) { 800 // Start a drag 801 final int start = mTextView.getSelectionStart(); 802 final int end = mTextView.getSelectionEnd(); 803 CharSequence selectedText = mTextView.getTransformedText(start, end); 804 ClipData data = ClipData.newPlainText(null, selectedText); 805 DragLocalState localState = new DragLocalState(mTextView, start, end); 806 mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0); 807 stopSelectionActionMode(); 808 } else { 809 getSelectionController().hide(); 810 selectCurrentWord(); 811 getSelectionController().show(); 812 } 813 handled = true; 814 } 815 816 // Start a new selection 817 if (!handled) { 818 handled = startSelectionActionMode(); 819 } 820 821 return handled; 822 } 823 824 private long getLastTouchOffsets() { 825 SelectionModifierCursorController selectionController = getSelectionController(); 826 final int minOffset = selectionController.getMinTouchOffset(); 827 final int maxOffset = selectionController.getMaxTouchOffset(); 828 return TextUtils.packRangeInLong(minOffset, maxOffset); 829 } 830 831 void onFocusChanged(boolean focused, int direction) { 832 mShowCursor = SystemClock.uptimeMillis(); 833 ensureEndedBatchEdit(); 834 835 if (focused) { 836 int selStart = mTextView.getSelectionStart(); 837 int selEnd = mTextView.getSelectionEnd(); 838 839 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection 840 // mode for these, unless there was a specific selection already started. 841 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 && 842 selEnd == mTextView.getText().length(); 843 844 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() && 845 !isFocusHighlighted; 846 847 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) { 848 // If a tap was used to give focus to that view, move cursor at tap position. 849 // Has to be done before onTakeFocus, which can be overloaded. 850 final int lastTapPosition = getLastTapPosition(); 851 if (lastTapPosition >= 0) { 852 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition); 853 } 854 855 // Note this may have to be moved out of the Editor class 856 MovementMethod mMovement = mTextView.getMovementMethod(); 857 if (mMovement != null) { 858 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction); 859 } 860 861 // The DecorView does not have focus when the 'Done' ExtractEditText button is 862 // pressed. Since it is the ViewAncestor's mView, it requests focus before 863 // ExtractEditText clears focus, which gives focus to the ExtractEditText. 864 // This special case ensure that we keep current selection in that case. 865 // It would be better to know why the DecorView does not have focus at that time. 866 if (((mTextView instanceof ExtractEditText) || mSelectionMoved) && 867 selStart >= 0 && selEnd >= 0) { 868 /* 869 * Someone intentionally set the selection, so let them 870 * do whatever it is that they wanted to do instead of 871 * the default on-focus behavior. We reset the selection 872 * here instead of just skipping the onTakeFocus() call 873 * because some movement methods do something other than 874 * just setting the selection in theirs and we still 875 * need to go through that path. 876 */ 877 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd); 878 } 879 880 if (mSelectAllOnFocus) { 881 mTextView.selectAllText(); 882 } 883 884 mTouchFocusSelected = true; 885 } 886 887 mFrozenWithFocus = false; 888 mSelectionMoved = false; 889 890 if (mError != null) { 891 showError(); 892 } 893 894 makeBlink(); 895 } else { 896 if (mError != null) { 897 hideError(); 898 } 899 // Don't leave us in the middle of a batch edit. 900 mTextView.onEndBatchEdit(); 901 902 if (mTextView instanceof ExtractEditText) { 903 // terminateTextSelectionMode removes selection, which we want to keep when 904 // ExtractEditText goes out of focus. 905 final int selStart = mTextView.getSelectionStart(); 906 final int selEnd = mTextView.getSelectionEnd(); 907 hideControllers(); 908 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd); 909 } else { 910 if (mTemporaryDetach) mPreserveDetachedSelection = true; 911 hideControllers(); 912 if (mTemporaryDetach) mPreserveDetachedSelection = false; 913 downgradeEasyCorrectionSpans(); 914 } 915 916 // No need to create the controller 917 if (mSelectionModifierCursorController != null) { 918 mSelectionModifierCursorController.resetTouchOffsets(); 919 } 920 } 921 } 922 923 /** 924 * Downgrades to simple suggestions all the easy correction spans that are not a spell check 925 * span. 926 */ 927 private void downgradeEasyCorrectionSpans() { 928 CharSequence text = mTextView.getText(); 929 if (text instanceof Spannable) { 930 Spannable spannable = (Spannable) text; 931 SuggestionSpan[] suggestionSpans = spannable.getSpans(0, 932 spannable.length(), SuggestionSpan.class); 933 for (int i = 0; i < suggestionSpans.length; i++) { 934 int flags = suggestionSpans[i].getFlags(); 935 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0 936 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) { 937 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT; 938 suggestionSpans[i].setFlags(flags); 939 } 940 } 941 } 942 } 943 944 void sendOnTextChanged(int start, int after) { 945 updateSpellCheckSpans(start, start + after, false); 946 947 // Hide the controllers as soon as text is modified (typing, procedural...) 948 // We do not hide the span controllers, since they can be added when a new text is 949 // inserted into the text view (voice IME). 950 hideCursorControllers(); 951 } 952 953 private int getLastTapPosition() { 954 // No need to create the controller at that point, no last tap position saved 955 if (mSelectionModifierCursorController != null) { 956 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset(); 957 if (lastTapPosition >= 0) { 958 // Safety check, should not be possible. 959 if (lastTapPosition > mTextView.getText().length()) { 960 lastTapPosition = mTextView.getText().length(); 961 } 962 return lastTapPosition; 963 } 964 } 965 966 return -1; 967 } 968 969 void onWindowFocusChanged(boolean hasWindowFocus) { 970 if (hasWindowFocus) { 971 if (mBlink != null) { 972 mBlink.uncancel(); 973 makeBlink(); 974 } 975 } else { 976 if (mBlink != null) { 977 mBlink.cancel(); 978 } 979 if (mInputContentType != null) { 980 mInputContentType.enterDown = false; 981 } 982 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp 983 hideControllers(); 984 if (mSuggestionsPopupWindow != null) { 985 mSuggestionsPopupWindow.onParentLostFocus(); 986 } 987 988 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged 989 ensureEndedBatchEdit(); 990 } 991 } 992 993 void onTouchEvent(MotionEvent event) { 994 if (hasSelectionController()) { 995 getSelectionController().onTouchEvent(event); 996 } 997 998 if (mShowSuggestionRunnable != null) { 999 mTextView.removeCallbacks(mShowSuggestionRunnable); 1000 mShowSuggestionRunnable = null; 1001 } 1002 1003 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 1004 mLastDownPositionX = event.getX(); 1005 mLastDownPositionY = event.getY(); 1006 1007 // Reset this state; it will be re-set if super.onTouchEvent 1008 // causes focus to move to the view. 1009 mTouchFocusSelected = false; 1010 mIgnoreActionUpEvent = false; 1011 } 1012 } 1013 1014 public void beginBatchEdit() { 1015 mInBatchEditControllers = true; 1016 final InputMethodState ims = mInputMethodState; 1017 if (ims != null) { 1018 int nesting = ++ims.mBatchEditNesting; 1019 if (nesting == 1) { 1020 ims.mCursorChanged = false; 1021 ims.mChangedDelta = 0; 1022 if (ims.mContentChanged) { 1023 // We already have a pending change from somewhere else, 1024 // so turn this into a full update. 1025 ims.mChangedStart = 0; 1026 ims.mChangedEnd = mTextView.getText().length(); 1027 } else { 1028 ims.mChangedStart = EXTRACT_UNKNOWN; 1029 ims.mChangedEnd = EXTRACT_UNKNOWN; 1030 ims.mContentChanged = false; 1031 } 1032 mTextView.onBeginBatchEdit(); 1033 } 1034 } 1035 } 1036 1037 public void endBatchEdit() { 1038 mInBatchEditControllers = false; 1039 final InputMethodState ims = mInputMethodState; 1040 if (ims != null) { 1041 int nesting = --ims.mBatchEditNesting; 1042 if (nesting == 0) { 1043 finishBatchEdit(ims); 1044 } 1045 } 1046 } 1047 1048 void ensureEndedBatchEdit() { 1049 final InputMethodState ims = mInputMethodState; 1050 if (ims != null && ims.mBatchEditNesting != 0) { 1051 ims.mBatchEditNesting = 0; 1052 finishBatchEdit(ims); 1053 } 1054 } 1055 1056 void finishBatchEdit(final InputMethodState ims) { 1057 mTextView.onEndBatchEdit(); 1058 1059 if (ims.mContentChanged || ims.mSelectionModeChanged) { 1060 mTextView.updateAfterEdit(); 1061 reportExtractedText(); 1062 } else if (ims.mCursorChanged) { 1063 // Cheezy way to get us to report the current cursor location. 1064 mTextView.invalidateCursor(); 1065 } 1066 } 1067 1068 static final int EXTRACT_NOTHING = -2; 1069 static final int EXTRACT_UNKNOWN = -1; 1070 1071 boolean extractText(ExtractedTextRequest request, ExtractedText outText) { 1072 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, 1073 EXTRACT_UNKNOWN, outText); 1074 } 1075 1076 private boolean extractTextInternal(ExtractedTextRequest request, 1077 int partialStartOffset, int partialEndOffset, int delta, 1078 ExtractedText outText) { 1079 final CharSequence content = mTextView.getText(); 1080 if (content != null) { 1081 if (partialStartOffset != EXTRACT_NOTHING) { 1082 final int N = content.length(); 1083 if (partialStartOffset < 0) { 1084 outText.partialStartOffset = outText.partialEndOffset = -1; 1085 partialStartOffset = 0; 1086 partialEndOffset = N; 1087 } else { 1088 // Now use the delta to determine the actual amount of text 1089 // we need. 1090 partialEndOffset += delta; 1091 // Adjust offsets to ensure we contain full spans. 1092 if (content instanceof Spanned) { 1093 Spanned spanned = (Spanned)content; 1094 Object[] spans = spanned.getSpans(partialStartOffset, 1095 partialEndOffset, ParcelableSpan.class); 1096 int i = spans.length; 1097 while (i > 0) { 1098 i--; 1099 int j = spanned.getSpanStart(spans[i]); 1100 if (j < partialStartOffset) partialStartOffset = j; 1101 j = spanned.getSpanEnd(spans[i]); 1102 if (j > partialEndOffset) partialEndOffset = j; 1103 } 1104 } 1105 outText.partialStartOffset = partialStartOffset; 1106 outText.partialEndOffset = partialEndOffset - delta; 1107 1108 if (partialStartOffset > N) { 1109 partialStartOffset = N; 1110 } else if (partialStartOffset < 0) { 1111 partialStartOffset = 0; 1112 } 1113 if (partialEndOffset > N) { 1114 partialEndOffset = N; 1115 } else if (partialEndOffset < 0) { 1116 partialEndOffset = 0; 1117 } 1118 } 1119 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { 1120 outText.text = content.subSequence(partialStartOffset, 1121 partialEndOffset); 1122 } else { 1123 outText.text = TextUtils.substring(content, partialStartOffset, 1124 partialEndOffset); 1125 } 1126 } else { 1127 outText.partialStartOffset = 0; 1128 outText.partialEndOffset = 0; 1129 outText.text = ""; 1130 } 1131 outText.flags = 0; 1132 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) { 1133 outText.flags |= ExtractedText.FLAG_SELECTING; 1134 } 1135 if (mTextView.isSingleLine()) { 1136 outText.flags |= ExtractedText.FLAG_SINGLE_LINE; 1137 } 1138 outText.startOffset = 0; 1139 outText.selectionStart = mTextView.getSelectionStart(); 1140 outText.selectionEnd = mTextView.getSelectionEnd(); 1141 return true; 1142 } 1143 return false; 1144 } 1145 1146 boolean reportExtractedText() { 1147 final Editor.InputMethodState ims = mInputMethodState; 1148 if (ims != null) { 1149 final boolean contentChanged = ims.mContentChanged; 1150 if (contentChanged || ims.mSelectionModeChanged) { 1151 ims.mContentChanged = false; 1152 ims.mSelectionModeChanged = false; 1153 final ExtractedTextRequest req = ims.mExtractedTextRequest; 1154 if (req != null) { 1155 InputMethodManager imm = InputMethodManager.peekInstance(); 1156 if (imm != null) { 1157 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG, 1158 "Retrieving extracted start=" + ims.mChangedStart + 1159 " end=" + ims.mChangedEnd + 1160 " delta=" + ims.mChangedDelta); 1161 if (ims.mChangedStart < 0 && !contentChanged) { 1162 ims.mChangedStart = EXTRACT_NOTHING; 1163 } 1164 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, 1165 ims.mChangedDelta, ims.mExtractedText)) { 1166 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG, 1167 "Reporting extracted start=" + 1168 ims.mExtractedText.partialStartOffset + 1169 " end=" + ims.mExtractedText.partialEndOffset + 1170 ": " + ims.mExtractedText.text); 1171 1172 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText); 1173 ims.mChangedStart = EXTRACT_UNKNOWN; 1174 ims.mChangedEnd = EXTRACT_UNKNOWN; 1175 ims.mChangedDelta = 0; 1176 ims.mContentChanged = false; 1177 return true; 1178 } 1179 } 1180 } 1181 } 1182 } 1183 return false; 1184 } 1185 1186 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint, 1187 int cursorOffsetVertical) { 1188 final int selectionStart = mTextView.getSelectionStart(); 1189 final int selectionEnd = mTextView.getSelectionEnd(); 1190 1191 final InputMethodState ims = mInputMethodState; 1192 if (ims != null && ims.mBatchEditNesting == 0) { 1193 InputMethodManager imm = InputMethodManager.peekInstance(); 1194 if (imm != null) { 1195 if (imm.isActive(mTextView)) { 1196 boolean reported = false; 1197 if (ims.mContentChanged || ims.mSelectionModeChanged) { 1198 // We are in extract mode and the content has changed 1199 // in some way... just report complete new text to the 1200 // input method. 1201 reported = reportExtractedText(); 1202 } 1203 if (!reported && highlight != null) { 1204 int candStart = -1; 1205 int candEnd = -1; 1206 if (mTextView.getText() instanceof Spannable) { 1207 Spannable sp = (Spannable) mTextView.getText(); 1208 candStart = EditableInputConnection.getComposingSpanStart(sp); 1209 candEnd = EditableInputConnection.getComposingSpanEnd(sp); 1210 } 1211 imm.updateSelection(mTextView, 1212 selectionStart, selectionEnd, candStart, candEnd); 1213 } 1214 } 1215 1216 if (imm.isWatchingCursor(mTextView) && highlight != null) { 1217 highlight.computeBounds(ims.mTmpRectF, true); 1218 ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0; 1219 1220 canvas.getMatrix().mapPoints(ims.mTmpOffset); 1221 ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]); 1222 1223 ims.mTmpRectF.offset(0, cursorOffsetVertical); 1224 1225 ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5), 1226 (int)(ims.mTmpRectF.top + 0.5), 1227 (int)(ims.mTmpRectF.right + 0.5), 1228 (int)(ims.mTmpRectF.bottom + 0.5)); 1229 1230 imm.updateCursor(mTextView, 1231 ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top, 1232 ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom); 1233 } 1234 } 1235 } 1236 1237 if (mCorrectionHighlighter != null) { 1238 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); 1239 } 1240 1241 if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) { 1242 drawCursor(canvas, cursorOffsetVertical); 1243 // Rely on the drawable entirely, do not draw the cursor line. 1244 // Has to be done after the IMM related code above which relies on the highlight. 1245 highlight = null; 1246 } 1247 1248 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) { 1249 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint, 1250 cursorOffsetVertical); 1251 } else { 1252 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical); 1253 } 1254 } 1255 1256 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight, 1257 Paint highlightPaint, int cursorOffsetVertical) { 1258 final long lineRange = layout.getLineRangeForDraw(canvas); 1259 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 1260 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 1261 if (lastLine < 0) return; 1262 1263 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical, 1264 firstLine, lastLine); 1265 1266 if (layout instanceof DynamicLayout) { 1267 if (mTextDisplayLists == null) { 1268 mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)]; 1269 } 1270 1271 // If the height of the layout changes (usually when inserting or deleting a line, 1272 // but could be changes within a span), invalidate everything. We could optimize 1273 // more aggressively (for example, adding offsets to blocks) but it would be more 1274 // complex and we would only get the benefit in some cases. 1275 int layoutHeight = layout.getHeight(); 1276 if (mLastLayoutHeight != layoutHeight) { 1277 invalidateTextDisplayList(); 1278 mLastLayoutHeight = layoutHeight; 1279 } 1280 1281 DynamicLayout dynamicLayout = (DynamicLayout) layout; 1282 int[] blockEndLines = dynamicLayout.getBlockEndLines(); 1283 int[] blockIndices = dynamicLayout.getBlockIndices(); 1284 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks(); 1285 1286 int endOfPreviousBlock = -1; 1287 int searchStartIndex = 0; 1288 for (int i = 0; i < numberOfBlocks; i++) { 1289 int blockEndLine = blockEndLines[i]; 1290 int blockIndex = blockIndices[i]; 1291 1292 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX; 1293 if (blockIsInvalid) { 1294 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks, 1295 searchStartIndex); 1296 // Note how dynamic layout's internal block indices get updated from Editor 1297 blockIndices[i] = blockIndex; 1298 searchStartIndex = blockIndex + 1; 1299 } 1300 1301 DisplayList blockDisplayList = mTextDisplayLists[blockIndex]; 1302 if (blockDisplayList == null) { 1303 blockDisplayList = mTextDisplayLists[blockIndex] = 1304 mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex); 1305 } else { 1306 if (blockIsInvalid) blockDisplayList.invalidate(); 1307 } 1308 1309 if (!blockDisplayList.isValid()) { 1310 final int blockBeginLine = endOfPreviousBlock + 1; 1311 final int top = layout.getLineTop(blockBeginLine); 1312 final int bottom = layout.getLineBottom(blockEndLine); 1313 int left = 0; 1314 int right = mTextView.getWidth(); 1315 if (mTextView.getHorizontallyScrolling()) { 1316 float min = Float.MAX_VALUE; 1317 float max = Float.MIN_VALUE; 1318 for (int line = blockBeginLine; line <= blockEndLine; line++) { 1319 min = Math.min(min, layout.getLineLeft(line)); 1320 max = Math.max(max, layout.getLineRight(line)); 1321 } 1322 left = (int) min; 1323 right = (int) (max + 0.5f); 1324 } 1325 1326 final HardwareCanvas hardwareCanvas = blockDisplayList.start(); 1327 try { 1328 // Tighten the bounds of the viewport to the actual text size 1329 hardwareCanvas.setViewport(right - left, bottom - top); 1330 // The dirty rect should always be null for a display list 1331 hardwareCanvas.onPreDraw(null); 1332 // drawText is always relative to TextView's origin, this translation brings 1333 // this range of text back to the top left corner of the viewport 1334 hardwareCanvas.translate(-left, -top); 1335 layout.drawText(hardwareCanvas, blockBeginLine, blockEndLine); 1336 // No need to untranslate, previous context is popped after drawDisplayList 1337 } finally { 1338 hardwareCanvas.onPostDraw(); 1339 blockDisplayList.end(); 1340 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom); 1341 // Same as drawDisplayList below, handled by our TextView's parent 1342 blockDisplayList.setClipChildren(false); 1343 } 1344 } 1345 1346 ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, null, 1347 0 /* no child clipping, our TextView parent enforces it */); 1348 1349 endOfPreviousBlock = blockEndLine; 1350 } 1351 } else { 1352 // Boring layout is used for empty and hint text 1353 layout.drawText(canvas, firstLine, lastLine); 1354 } 1355 } 1356 1357 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks, 1358 int searchStartIndex) { 1359 int length = mTextDisplayLists.length; 1360 for (int i = searchStartIndex; i < length; i++) { 1361 boolean blockIndexFound = false; 1362 for (int j = 0; j < numberOfBlocks; j++) { 1363 if (blockIndices[j] == i) { 1364 blockIndexFound = true; 1365 break; 1366 } 1367 } 1368 if (blockIndexFound) continue; 1369 return i; 1370 } 1371 1372 // No available index found, the pool has to grow 1373 int newSize = ArrayUtils.idealIntArraySize(length + 1); 1374 DisplayList[] displayLists = new DisplayList[newSize]; 1375 System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length); 1376 mTextDisplayLists = displayLists; 1377 return length; 1378 } 1379 1380 private void drawCursor(Canvas canvas, int cursorOffsetVertical) { 1381 final boolean translate = cursorOffsetVertical != 0; 1382 if (translate) canvas.translate(0, cursorOffsetVertical); 1383 for (int i = 0; i < mCursorCount; i++) { 1384 mCursorDrawable[i].draw(canvas); 1385 } 1386 if (translate) canvas.translate(0, -cursorOffsetVertical); 1387 } 1388 1389 /** 1390 * Invalidates all the sub-display lists that overlap the specified character range 1391 */ 1392 void invalidateTextDisplayList(Layout layout, int start, int end) { 1393 if (mTextDisplayLists != null && layout instanceof DynamicLayout) { 1394 final int firstLine = layout.getLineForOffset(start); 1395 final int lastLine = layout.getLineForOffset(end); 1396 1397 DynamicLayout dynamicLayout = (DynamicLayout) layout; 1398 int[] blockEndLines = dynamicLayout.getBlockEndLines(); 1399 int[] blockIndices = dynamicLayout.getBlockIndices(); 1400 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks(); 1401 1402 int i = 0; 1403 // Skip the blocks before firstLine 1404 while (i < numberOfBlocks) { 1405 if (blockEndLines[i] >= firstLine) break; 1406 i++; 1407 } 1408 1409 // Invalidate all subsequent blocks until lastLine is passed 1410 while (i < numberOfBlocks) { 1411 final int blockIndex = blockIndices[i]; 1412 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) { 1413 mTextDisplayLists[blockIndex].invalidate(); 1414 } 1415 if (blockEndLines[i] >= lastLine) break; 1416 i++; 1417 } 1418 } 1419 } 1420 1421 void invalidateTextDisplayList() { 1422 if (mTextDisplayLists != null) { 1423 for (int i = 0; i < mTextDisplayLists.length; i++) { 1424 if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate(); 1425 } 1426 } 1427 } 1428 1429 void updateCursorsPositions() { 1430 if (mTextView.mCursorDrawableRes == 0) { 1431 mCursorCount = 0; 1432 return; 1433 } 1434 1435 Layout layout = mTextView.getLayout(); 1436 Layout hintLayout = mTextView.getHintLayout(); 1437 final int offset = mTextView.getSelectionStart(); 1438 final int line = layout.getLineForOffset(offset); 1439 final int top = layout.getLineTop(line); 1440 final int bottom = layout.getLineTop(line + 1); 1441 1442 mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1; 1443 1444 int middle = bottom; 1445 if (mCursorCount == 2) { 1446 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)} 1447 middle = (top + bottom) >> 1; 1448 } 1449 1450 updateCursorPosition(0, top, middle, getPrimaryHorizontal(layout, hintLayout, offset)); 1451 1452 if (mCursorCount == 2) { 1453 updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset)); 1454 } 1455 } 1456 1457 private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset) { 1458 if (TextUtils.isEmpty(layout.getText()) && 1459 hintLayout != null && 1460 !TextUtils.isEmpty(hintLayout.getText())) { 1461 return hintLayout.getPrimaryHorizontal(offset); 1462 } else { 1463 return layout.getPrimaryHorizontal(offset); 1464 } 1465 } 1466 1467 /** 1468 * @return true if the selection mode was actually started. 1469 */ 1470 boolean startSelectionActionMode() { 1471 if (mSelectionActionMode != null) { 1472 // Selection action mode is already started 1473 return false; 1474 } 1475 1476 if (!canSelectText() || !mTextView.requestFocus()) { 1477 Log.w(TextView.LOG_TAG, 1478 "TextView does not support text selection. Action mode cancelled."); 1479 return false; 1480 } 1481 1482 if (!mTextView.hasSelection()) { 1483 // There may already be a selection on device rotation 1484 if (!selectCurrentWord()) { 1485 // No word found under cursor or text selection not permitted. 1486 return false; 1487 } 1488 } 1489 1490 boolean willExtract = extractedTextModeWillBeStarted(); 1491 1492 // Do not start the action mode when extracted text will show up full screen, which would 1493 // immediately hide the newly created action bar and would be visually distracting. 1494 if (!willExtract) { 1495 ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); 1496 mSelectionActionMode = mTextView.startActionMode(actionModeCallback); 1497 } 1498 1499 final boolean selectionStarted = mSelectionActionMode != null || willExtract; 1500 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) { 1501 // Show the IME to be able to replace text, except when selecting non editable text. 1502 final InputMethodManager imm = InputMethodManager.peekInstance(); 1503 if (imm != null) { 1504 imm.showSoftInput(mTextView, 0, null); 1505 } 1506 } 1507 1508 return selectionStarted; 1509 } 1510 1511 private boolean extractedTextModeWillBeStarted() { 1512 if (!(mTextView instanceof ExtractEditText)) { 1513 final InputMethodManager imm = InputMethodManager.peekInstance(); 1514 return imm != null && imm.isFullscreenMode(); 1515 } 1516 return false; 1517 } 1518 1519 /** 1520 * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}. 1521 */ 1522 private boolean isCursorInsideSuggestionSpan() { 1523 CharSequence text = mTextView.getText(); 1524 if (!(text instanceof Spannable)) return false; 1525 1526 SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans( 1527 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class); 1528 return (suggestionSpans.length > 0); 1529 } 1530 1531 /** 1532 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with 1533 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set. 1534 */ 1535 private boolean isCursorInsideEasyCorrectionSpan() { 1536 Spannable spannable = (Spannable) mTextView.getText(); 1537 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(), 1538 mTextView.getSelectionEnd(), SuggestionSpan.class); 1539 for (int i = 0; i < suggestionSpans.length; i++) { 1540 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) { 1541 return true; 1542 } 1543 } 1544 return false; 1545 } 1546 1547 void onTouchUpEvent(MotionEvent event) { 1548 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect(); 1549 hideControllers(); 1550 CharSequence text = mTextView.getText(); 1551 if (!selectAllGotFocus && text.length() > 0) { 1552 // Move cursor 1553 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); 1554 Selection.setSelection((Spannable) text, offset); 1555 if (mSpellChecker != null) { 1556 // When the cursor moves, the word that was typed may need spell check 1557 mSpellChecker.onSelectionChanged(); 1558 } 1559 if (!extractedTextModeWillBeStarted()) { 1560 if (isCursorInsideEasyCorrectionSpan()) { 1561 mShowSuggestionRunnable = new Runnable() { 1562 public void run() { 1563 showSuggestions(); 1564 } 1565 }; 1566 // removeCallbacks is performed on every touch 1567 mTextView.postDelayed(mShowSuggestionRunnable, 1568 ViewConfiguration.getDoubleTapTimeout()); 1569 } else if (hasInsertionController()) { 1570 getInsertionController().show(); 1571 } 1572 } 1573 } 1574 } 1575 1576 protected void stopSelectionActionMode() { 1577 if (mSelectionActionMode != null) { 1578 // This will hide the mSelectionModifierCursorController 1579 mSelectionActionMode.finish(); 1580 } 1581 } 1582 1583 /** 1584 * @return True if this view supports insertion handles. 1585 */ 1586 boolean hasInsertionController() { 1587 return mInsertionControllerEnabled; 1588 } 1589 1590 /** 1591 * @return True if this view supports selection handles. 1592 */ 1593 boolean hasSelectionController() { 1594 return mSelectionControllerEnabled; 1595 } 1596 1597 InsertionPointCursorController getInsertionController() { 1598 if (!mInsertionControllerEnabled) { 1599 return null; 1600 } 1601 1602 if (mInsertionPointCursorController == null) { 1603 mInsertionPointCursorController = new InsertionPointCursorController(); 1604 1605 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 1606 observer.addOnTouchModeChangeListener(mInsertionPointCursorController); 1607 } 1608 1609 return mInsertionPointCursorController; 1610 } 1611 1612 SelectionModifierCursorController getSelectionController() { 1613 if (!mSelectionControllerEnabled) { 1614 return null; 1615 } 1616 1617 if (mSelectionModifierCursorController == null) { 1618 mSelectionModifierCursorController = new SelectionModifierCursorController(); 1619 1620 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 1621 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); 1622 } 1623 1624 return mSelectionModifierCursorController; 1625 } 1626 1627 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) { 1628 if (mCursorDrawable[cursorIndex] == null) 1629 mCursorDrawable[cursorIndex] = mTextView.getResources().getDrawable( 1630 mTextView.mCursorDrawableRes); 1631 1632 if (mTempRect == null) mTempRect = new Rect(); 1633 mCursorDrawable[cursorIndex].getPadding(mTempRect); 1634 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth(); 1635 horizontal = Math.max(0.5f, horizontal - 0.5f); 1636 final int left = (int) (horizontal) - mTempRect.left; 1637 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width, 1638 bottom + mTempRect.bottom); 1639 } 1640 1641 /** 1642 * Called by the framework in response to a text auto-correction (such as fixing a typo using a 1643 * a dictionnary) from the current input method, provided by it calling 1644 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default 1645 * implementation flashes the background of the corrected word to provide feedback to the user. 1646 * 1647 * @param info The auto correct info about the text that was corrected. 1648 */ 1649 public void onCommitCorrection(CorrectionInfo info) { 1650 if (mCorrectionHighlighter == null) { 1651 mCorrectionHighlighter = new CorrectionHighlighter(); 1652 } else { 1653 mCorrectionHighlighter.invalidate(false); 1654 } 1655 1656 mCorrectionHighlighter.highlight(info); 1657 } 1658 1659 void showSuggestions() { 1660 if (mSuggestionsPopupWindow == null) { 1661 mSuggestionsPopupWindow = new SuggestionsPopupWindow(); 1662 } 1663 hideControllers(); 1664 mSuggestionsPopupWindow.show(); 1665 } 1666 1667 boolean areSuggestionsShown() { 1668 return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing(); 1669 } 1670 1671 void onScrollChanged() { 1672 if (mPositionListener != null) { 1673 mPositionListener.onScrollChanged(); 1674 } 1675 } 1676 1677 /** 1678 * @return True when the TextView isFocused and has a valid zero-length selection (cursor). 1679 */ 1680 private boolean shouldBlink() { 1681 if (!isCursorVisible() || !mTextView.isFocused()) return false; 1682 1683 final int start = mTextView.getSelectionStart(); 1684 if (start < 0) return false; 1685 1686 final int end = mTextView.getSelectionEnd(); 1687 if (end < 0) return false; 1688 1689 return start == end; 1690 } 1691 1692 void makeBlink() { 1693 if (shouldBlink()) { 1694 mShowCursor = SystemClock.uptimeMillis(); 1695 if (mBlink == null) mBlink = new Blink(); 1696 mBlink.removeCallbacks(mBlink); 1697 mBlink.postAtTime(mBlink, mShowCursor + BLINK); 1698 } else { 1699 if (mBlink != null) mBlink.removeCallbacks(mBlink); 1700 } 1701 } 1702 1703 private class Blink extends Handler implements Runnable { 1704 private boolean mCancelled; 1705 1706 public void run() { 1707 if (mCancelled) { 1708 return; 1709 } 1710 1711 removeCallbacks(Blink.this); 1712 1713 if (shouldBlink()) { 1714 if (mTextView.getLayout() != null) { 1715 mTextView.invalidateCursorPath(); 1716 } 1717 1718 postAtTime(this, SystemClock.uptimeMillis() + BLINK); 1719 } 1720 } 1721 1722 void cancel() { 1723 if (!mCancelled) { 1724 removeCallbacks(Blink.this); 1725 mCancelled = true; 1726 } 1727 } 1728 1729 void uncancel() { 1730 mCancelled = false; 1731 } 1732 } 1733 1734 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) { 1735 TextView shadowView = (TextView) View.inflate(mTextView.getContext(), 1736 com.android.internal.R.layout.text_drag_thumbnail, null); 1737 1738 if (shadowView == null) { 1739 throw new IllegalArgumentException("Unable to inflate text drag thumbnail"); 1740 } 1741 1742 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) { 1743 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH); 1744 } 1745 shadowView.setText(text); 1746 shadowView.setTextColor(mTextView.getTextColors()); 1747 1748 shadowView.setTextAppearance(mTextView.getContext(), R.styleable.Theme_textAppearanceLarge); 1749 shadowView.setGravity(Gravity.CENTER); 1750 1751 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1752 ViewGroup.LayoutParams.WRAP_CONTENT)); 1753 1754 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 1755 shadowView.measure(size, size); 1756 1757 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight()); 1758 shadowView.invalidate(); 1759 return new DragShadowBuilder(shadowView); 1760 } 1761 1762 private static class DragLocalState { 1763 public TextView sourceTextView; 1764 public int start, end; 1765 1766 public DragLocalState(TextView sourceTextView, int start, int end) { 1767 this.sourceTextView = sourceTextView; 1768 this.start = start; 1769 this.end = end; 1770 } 1771 } 1772 1773 void onDrop(DragEvent event) { 1774 StringBuilder content = new StringBuilder(""); 1775 ClipData clipData = event.getClipData(); 1776 final int itemCount = clipData.getItemCount(); 1777 for (int i=0; i < itemCount; i++) { 1778 Item item = clipData.getItemAt(i); 1779 content.append(item.coerceToStyledText(mTextView.getContext())); 1780 } 1781 1782 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); 1783 1784 Object localState = event.getLocalState(); 1785 DragLocalState dragLocalState = null; 1786 if (localState instanceof DragLocalState) { 1787 dragLocalState = (DragLocalState) localState; 1788 } 1789 boolean dragDropIntoItself = dragLocalState != null && 1790 dragLocalState.sourceTextView == mTextView; 1791 1792 if (dragDropIntoItself) { 1793 if (offset >= dragLocalState.start && offset < dragLocalState.end) { 1794 // A drop inside the original selection discards the drop. 1795 return; 1796 } 1797 } 1798 1799 final int originalLength = mTextView.getText().length(); 1800 long minMax = mTextView.prepareSpacesAroundPaste(offset, offset, content); 1801 int min = TextUtils.unpackRangeStartFromLong(minMax); 1802 int max = TextUtils.unpackRangeEndFromLong(minMax); 1803 1804 Selection.setSelection((Spannable) mTextView.getText(), max); 1805 mTextView.replaceText_internal(min, max, content); 1806 1807 if (dragDropIntoItself) { 1808 int dragSourceStart = dragLocalState.start; 1809 int dragSourceEnd = dragLocalState.end; 1810 if (max <= dragSourceStart) { 1811 // Inserting text before selection has shifted positions 1812 final int shift = mTextView.getText().length() - originalLength; 1813 dragSourceStart += shift; 1814 dragSourceEnd += shift; 1815 } 1816 1817 // Delete original selection 1818 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd); 1819 1820 // Make sure we do not leave two adjacent spaces. 1821 final int prevCharIdx = Math.max(0, dragSourceStart - 1); 1822 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1); 1823 if (nextCharIdx > prevCharIdx + 1) { 1824 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx); 1825 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) { 1826 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1); 1827 } 1828 } 1829 } 1830 } 1831 1832 public void addSpanWatchers(Spannable text) { 1833 final int textLength = text.length(); 1834 1835 if (mKeyListener != null) { 1836 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); 1837 } 1838 1839 if (mEasyEditSpanController == null) { 1840 mEasyEditSpanController = new EasyEditSpanController(); 1841 } 1842 text.setSpan(mEasyEditSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); 1843 } 1844 1845 /** 1846 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related 1847 * pop-up should be displayed. 1848 */ 1849 class EasyEditSpanController implements SpanWatcher { 1850 1851 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs 1852 1853 private EasyEditPopupWindow mPopupWindow; 1854 1855 private Runnable mHidePopup; 1856 1857 @Override 1858 public void onSpanAdded(Spannable text, Object span, int start, int end) { 1859 if (span instanceof EasyEditSpan) { 1860 if (mPopupWindow == null) { 1861 mPopupWindow = new EasyEditPopupWindow(); 1862 mHidePopup = new Runnable() { 1863 @Override 1864 public void run() { 1865 hide(); 1866 } 1867 }; 1868 } 1869 1870 // Make sure there is only at most one EasyEditSpan in the text 1871 if (mPopupWindow.mEasyEditSpan != null) { 1872 text.removeSpan(mPopupWindow.mEasyEditSpan); 1873 } 1874 1875 mPopupWindow.setEasyEditSpan((EasyEditSpan) span); 1876 1877 if (mTextView.getWindowVisibility() != View.VISIBLE) { 1878 // The window is not visible yet, ignore the text change. 1879 return; 1880 } 1881 1882 if (mTextView.getLayout() == null) { 1883 // The view has not been laid out yet, ignore the text change 1884 return; 1885 } 1886 1887 if (extractedTextModeWillBeStarted()) { 1888 // The input is in extract mode. Do not handle the easy edit in 1889 // the original TextView, as the ExtractEditText will do 1890 return; 1891 } 1892 1893 mPopupWindow.show(); 1894 mTextView.removeCallbacks(mHidePopup); 1895 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS); 1896 } 1897 } 1898 1899 @Override 1900 public void onSpanRemoved(Spannable text, Object span, int start, int end) { 1901 if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) { 1902 hide(); 1903 } 1904 } 1905 1906 @Override 1907 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd, 1908 int newStart, int newEnd) { 1909 if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) { 1910 text.removeSpan(mPopupWindow.mEasyEditSpan); 1911 } 1912 } 1913 1914 public void hide() { 1915 if (mPopupWindow != null) { 1916 mPopupWindow.hide(); 1917 mTextView.removeCallbacks(mHidePopup); 1918 } 1919 } 1920 } 1921 1922 /** 1923 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled 1924 * by {@link EasyEditSpanController}. 1925 */ 1926 private class EasyEditPopupWindow extends PinnedPopupWindow 1927 implements OnClickListener { 1928 private static final int POPUP_TEXT_LAYOUT = 1929 com.android.internal.R.layout.text_edit_action_popup_text; 1930 private TextView mDeleteTextView; 1931 private EasyEditSpan mEasyEditSpan; 1932 1933 @Override 1934 protected void createPopupWindow() { 1935 mPopupWindow = new PopupWindow(mTextView.getContext(), null, 1936 com.android.internal.R.attr.textSelectHandleWindowStyle); 1937 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1938 mPopupWindow.setClippingEnabled(true); 1939 } 1940 1941 @Override 1942 protected void initContentView() { 1943 LinearLayout linearLayout = new LinearLayout(mTextView.getContext()); 1944 linearLayout.setOrientation(LinearLayout.HORIZONTAL); 1945 mContentView = linearLayout; 1946 mContentView.setBackgroundResource( 1947 com.android.internal.R.drawable.text_edit_side_paste_window); 1948 1949 LayoutInflater inflater = (LayoutInflater)mTextView.getContext(). 1950 getSystemService(Context.LAYOUT_INFLATER_SERVICE); 1951 1952 LayoutParams wrapContent = new LayoutParams( 1953 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1954 1955 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); 1956 mDeleteTextView.setLayoutParams(wrapContent); 1957 mDeleteTextView.setText(com.android.internal.R.string.delete); 1958 mDeleteTextView.setOnClickListener(this); 1959 mContentView.addView(mDeleteTextView); 1960 } 1961 1962 public void setEasyEditSpan(EasyEditSpan easyEditSpan) { 1963 mEasyEditSpan = easyEditSpan; 1964 } 1965 1966 @Override 1967 public void onClick(View view) { 1968 if (view == mDeleteTextView) { 1969 Editable editable = (Editable) mTextView.getText(); 1970 int start = editable.getSpanStart(mEasyEditSpan); 1971 int end = editable.getSpanEnd(mEasyEditSpan); 1972 if (start >= 0 && end >= 0) { 1973 mTextView.deleteText_internal(start, end); 1974 } 1975 } 1976 } 1977 1978 @Override 1979 protected int getTextOffset() { 1980 // Place the pop-up at the end of the span 1981 Editable editable = (Editable) mTextView.getText(); 1982 return editable.getSpanEnd(mEasyEditSpan); 1983 } 1984 1985 @Override 1986 protected int getVerticalLocalPosition(int line) { 1987 return mTextView.getLayout().getLineBottom(line); 1988 } 1989 1990 @Override 1991 protected int clipVertically(int positionY) { 1992 // As we display the pop-up below the span, no vertical clipping is required. 1993 return positionY; 1994 } 1995 } 1996 1997 private class PositionListener implements ViewTreeObserver.OnPreDrawListener { 1998 // 3 handles 1999 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others) 2000 private final int MAXIMUM_NUMBER_OF_LISTENERS = 6; 2001 private TextViewPositionListener[] mPositionListeners = 2002 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS]; 2003 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS]; 2004 private boolean mPositionHasChanged = true; 2005 // Absolute position of the TextView with respect to its parent window 2006 private int mPositionX, mPositionY; 2007 private int mNumberOfListeners; 2008 private boolean mScrollHasChanged; 2009 final int[] mTempCoords = new int[2]; 2010 2011 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) { 2012 if (mNumberOfListeners == 0) { 2013 updatePosition(); 2014 ViewTreeObserver vto = mTextView.getViewTreeObserver(); 2015 vto.addOnPreDrawListener(this); 2016 } 2017 2018 int emptySlotIndex = -1; 2019 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 2020 TextViewPositionListener listener = mPositionListeners[i]; 2021 if (listener == positionListener) { 2022 return; 2023 } else if (emptySlotIndex < 0 && listener == null) { 2024 emptySlotIndex = i; 2025 } 2026 } 2027 2028 mPositionListeners[emptySlotIndex] = positionListener; 2029 mCanMove[emptySlotIndex] = canMove; 2030 mNumberOfListeners++; 2031 } 2032 2033 public void removeSubscriber(TextViewPositionListener positionListener) { 2034 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 2035 if (mPositionListeners[i] == positionListener) { 2036 mPositionListeners[i] = null; 2037 mNumberOfListeners--; 2038 break; 2039 } 2040 } 2041 2042 if (mNumberOfListeners == 0) { 2043 ViewTreeObserver vto = mTextView.getViewTreeObserver(); 2044 vto.removeOnPreDrawListener(this); 2045 } 2046 } 2047 2048 public int getPositionX() { 2049 return mPositionX; 2050 } 2051 2052 public int getPositionY() { 2053 return mPositionY; 2054 } 2055 2056 @Override 2057 public boolean onPreDraw() { 2058 updatePosition(); 2059 2060 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 2061 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) { 2062 TextViewPositionListener positionListener = mPositionListeners[i]; 2063 if (positionListener != null) { 2064 positionListener.updatePosition(mPositionX, mPositionY, 2065 mPositionHasChanged, mScrollHasChanged); 2066 } 2067 } 2068 } 2069 2070 mScrollHasChanged = false; 2071 return true; 2072 } 2073 2074 private void updatePosition() { 2075 mTextView.getLocationInWindow(mTempCoords); 2076 2077 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY; 2078 2079 mPositionX = mTempCoords[0]; 2080 mPositionY = mTempCoords[1]; 2081 } 2082 2083 public void onScrollChanged() { 2084 mScrollHasChanged = true; 2085 } 2086 } 2087 2088 private abstract class PinnedPopupWindow implements TextViewPositionListener { 2089 protected PopupWindow mPopupWindow; 2090 protected ViewGroup mContentView; 2091 int mPositionX, mPositionY; 2092 2093 protected abstract void createPopupWindow(); 2094 protected abstract void initContentView(); 2095 protected abstract int getTextOffset(); 2096 protected abstract int getVerticalLocalPosition(int line); 2097 protected abstract int clipVertically(int positionY); 2098 2099 public PinnedPopupWindow() { 2100 createPopupWindow(); 2101 2102 mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); 2103 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); 2104 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); 2105 2106 initContentView(); 2107 2108 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 2109 ViewGroup.LayoutParams.WRAP_CONTENT); 2110 mContentView.setLayoutParams(wrapContent); 2111 2112 mPopupWindow.setContentView(mContentView); 2113 } 2114 2115 public void show() { 2116 getPositionListener().addSubscriber(this, false /* offset is fixed */); 2117 2118 computeLocalPosition(); 2119 2120 final PositionListener positionListener = getPositionListener(); 2121 updatePosition(positionListener.getPositionX(), positionListener.getPositionY()); 2122 } 2123 2124 protected void measureContent() { 2125 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 2126 mContentView.measure( 2127 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, 2128 View.MeasureSpec.AT_MOST), 2129 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, 2130 View.MeasureSpec.AT_MOST)); 2131 } 2132 2133 /* The popup window will be horizontally centered on the getTextOffset() and vertically 2134 * positioned according to viewportToContentHorizontalOffset. 2135 * 2136 * This method assumes that mContentView has properly been measured from its content. */ 2137 private void computeLocalPosition() { 2138 measureContent(); 2139 final int width = mContentView.getMeasuredWidth(); 2140 final int offset = getTextOffset(); 2141 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f); 2142 mPositionX += mTextView.viewportToContentHorizontalOffset(); 2143 2144 final int line = mTextView.getLayout().getLineForOffset(offset); 2145 mPositionY = getVerticalLocalPosition(line); 2146 mPositionY += mTextView.viewportToContentVerticalOffset(); 2147 } 2148 2149 private void updatePosition(int parentPositionX, int parentPositionY) { 2150 int positionX = parentPositionX + mPositionX; 2151 int positionY = parentPositionY + mPositionY; 2152 2153 positionY = clipVertically(positionY); 2154 2155 // Horizontal clipping 2156 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 2157 final int width = mContentView.getMeasuredWidth(); 2158 positionX = Math.min(displayMetrics.widthPixels - width, positionX); 2159 positionX = Math.max(0, positionX); 2160 2161 if (isShowing()) { 2162 mPopupWindow.update(positionX, positionY, -1, -1); 2163 } else { 2164 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, 2165 positionX, positionY); 2166 } 2167 } 2168 2169 public void hide() { 2170 mPopupWindow.dismiss(); 2171 getPositionListener().removeSubscriber(this); 2172 } 2173 2174 @Override 2175 public void updatePosition(int parentPositionX, int parentPositionY, 2176 boolean parentPositionChanged, boolean parentScrolled) { 2177 // Either parentPositionChanged or parentScrolled is true, check if still visible 2178 if (isShowing() && isOffsetVisible(getTextOffset())) { 2179 if (parentScrolled) computeLocalPosition(); 2180 updatePosition(parentPositionX, parentPositionY); 2181 } else { 2182 hide(); 2183 } 2184 } 2185 2186 public boolean isShowing() { 2187 return mPopupWindow.isShowing(); 2188 } 2189 } 2190 2191 private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener { 2192 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE; 2193 private static final int ADD_TO_DICTIONARY = -1; 2194 private static final int DELETE_TEXT = -2; 2195 private SuggestionInfo[] mSuggestionInfos; 2196 private int mNumberOfSuggestions; 2197 private boolean mCursorWasVisibleBeforeSuggestions; 2198 private boolean mIsShowingUp = false; 2199 private SuggestionAdapter mSuggestionsAdapter; 2200 private final Comparator<SuggestionSpan> mSuggestionSpanComparator; 2201 private final HashMap<SuggestionSpan, Integer> mSpansLengths; 2202 2203 private class CustomPopupWindow extends PopupWindow { 2204 public CustomPopupWindow(Context context, int defStyle) { 2205 super(context, null, defStyle); 2206 } 2207 2208 @Override 2209 public void dismiss() { 2210 super.dismiss(); 2211 2212 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this); 2213 2214 // Safe cast since show() checks that mTextView.getText() is an Editable 2215 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan); 2216 2217 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions); 2218 if (hasInsertionController()) { 2219 getInsertionController().show(); 2220 } 2221 } 2222 } 2223 2224 public SuggestionsPopupWindow() { 2225 mCursorWasVisibleBeforeSuggestions = mCursorVisible; 2226 mSuggestionSpanComparator = new SuggestionSpanComparator(); 2227 mSpansLengths = new HashMap<SuggestionSpan, Integer>(); 2228 } 2229 2230 @Override 2231 protected void createPopupWindow() { 2232 mPopupWindow = new CustomPopupWindow(mTextView.getContext(), 2233 com.android.internal.R.attr.textSuggestionsWindowStyle); 2234 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 2235 mPopupWindow.setFocusable(true); 2236 mPopupWindow.setClippingEnabled(false); 2237 } 2238 2239 @Override 2240 protected void initContentView() { 2241 ListView listView = new ListView(mTextView.getContext()); 2242 mSuggestionsAdapter = new SuggestionAdapter(); 2243 listView.setAdapter(mSuggestionsAdapter); 2244 listView.setOnItemClickListener(this); 2245 mContentView = listView; 2246 2247 // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete 2248 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2]; 2249 for (int i = 0; i < mSuggestionInfos.length; i++) { 2250 mSuggestionInfos[i] = new SuggestionInfo(); 2251 } 2252 } 2253 2254 public boolean isShowingUp() { 2255 return mIsShowingUp; 2256 } 2257 2258 public void onParentLostFocus() { 2259 mIsShowingUp = false; 2260 } 2261 2262 private class SuggestionInfo { 2263 int suggestionStart, suggestionEnd; // range of actual suggestion within text 2264 SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents 2265 int suggestionIndex; // the index of this suggestion inside suggestionSpan 2266 SpannableStringBuilder text = new SpannableStringBuilder(); 2267 TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(), 2268 android.R.style.TextAppearance_SuggestionHighlight); 2269 } 2270 2271 private class SuggestionAdapter extends BaseAdapter { 2272 private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext(). 2273 getSystemService(Context.LAYOUT_INFLATER_SERVICE); 2274 2275 @Override 2276 public int getCount() { 2277 return mNumberOfSuggestions; 2278 } 2279 2280 @Override 2281 public Object getItem(int position) { 2282 return mSuggestionInfos[position]; 2283 } 2284 2285 @Override 2286 public long getItemId(int position) { 2287 return position; 2288 } 2289 2290 @Override 2291 public View getView(int position, View convertView, ViewGroup parent) { 2292 TextView textView = (TextView) convertView; 2293 2294 if (textView == null) { 2295 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout, 2296 parent, false); 2297 } 2298 2299 final SuggestionInfo suggestionInfo = mSuggestionInfos[position]; 2300 textView.setText(suggestionInfo.text); 2301 2302 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY || 2303 suggestionInfo.suggestionIndex == DELETE_TEXT) { 2304 textView.setBackgroundColor(Color.TRANSPARENT); 2305 } else { 2306 textView.setBackgroundColor(Color.WHITE); 2307 } 2308 2309 return textView; 2310 } 2311 } 2312 2313 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> { 2314 public int compare(SuggestionSpan span1, SuggestionSpan span2) { 2315 final int flag1 = span1.getFlags(); 2316 final int flag2 = span2.getFlags(); 2317 if (flag1 != flag2) { 2318 // The order here should match what is used in updateDrawState 2319 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; 2320 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; 2321 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0; 2322 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0; 2323 if (easy1 && !misspelled1) return -1; 2324 if (easy2 && !misspelled2) return 1; 2325 if (misspelled1) return -1; 2326 if (misspelled2) return 1; 2327 } 2328 2329 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue(); 2330 } 2331 } 2332 2333 /** 2334 * Returns the suggestion spans that cover the current cursor position. The suggestion 2335 * spans are sorted according to the length of text that they are attached to. 2336 */ 2337 private SuggestionSpan[] getSuggestionSpans() { 2338 int pos = mTextView.getSelectionStart(); 2339 Spannable spannable = (Spannable) mTextView.getText(); 2340 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); 2341 2342 mSpansLengths.clear(); 2343 for (SuggestionSpan suggestionSpan : suggestionSpans) { 2344 int start = spannable.getSpanStart(suggestionSpan); 2345 int end = spannable.getSpanEnd(suggestionSpan); 2346 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start)); 2347 } 2348 2349 // The suggestions are sorted according to their types (easy correction first, then 2350 // misspelled) and to the length of the text that they cover (shorter first). 2351 Arrays.sort(suggestionSpans, mSuggestionSpanComparator); 2352 return suggestionSpans; 2353 } 2354 2355 @Override 2356 public void show() { 2357 if (!(mTextView.getText() instanceof Editable)) return; 2358 2359 if (updateSuggestions()) { 2360 mCursorWasVisibleBeforeSuggestions = mCursorVisible; 2361 mTextView.setCursorVisible(false); 2362 mIsShowingUp = true; 2363 super.show(); 2364 } 2365 } 2366 2367 @Override 2368 protected void measureContent() { 2369 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 2370 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec( 2371 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST); 2372 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec( 2373 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST); 2374 2375 int width = 0; 2376 View view = null; 2377 for (int i = 0; i < mNumberOfSuggestions; i++) { 2378 view = mSuggestionsAdapter.getView(i, view, mContentView); 2379 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT; 2380 view.measure(horizontalMeasure, verticalMeasure); 2381 width = Math.max(width, view.getMeasuredWidth()); 2382 } 2383 2384 // Enforce the width based on actual text widths 2385 mContentView.measure( 2386 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), 2387 verticalMeasure); 2388 2389 Drawable popupBackground = mPopupWindow.getBackground(); 2390 if (popupBackground != null) { 2391 if (mTempRect == null) mTempRect = new Rect(); 2392 popupBackground.getPadding(mTempRect); 2393 width += mTempRect.left + mTempRect.right; 2394 } 2395 mPopupWindow.setWidth(width); 2396 } 2397 2398 @Override 2399 protected int getTextOffset() { 2400 return mTextView.getSelectionStart(); 2401 } 2402 2403 @Override 2404 protected int getVerticalLocalPosition(int line) { 2405 return mTextView.getLayout().getLineBottom(line); 2406 } 2407 2408 @Override 2409 protected int clipVertically(int positionY) { 2410 final int height = mContentView.getMeasuredHeight(); 2411 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 2412 return Math.min(positionY, displayMetrics.heightPixels - height); 2413 } 2414 2415 @Override 2416 public void hide() { 2417 super.hide(); 2418 } 2419 2420 private boolean updateSuggestions() { 2421 Spannable spannable = (Spannable) mTextView.getText(); 2422 SuggestionSpan[] suggestionSpans = getSuggestionSpans(); 2423 2424 final int nbSpans = suggestionSpans.length; 2425 // Suggestions are shown after a delay: the underlying spans may have been removed 2426 if (nbSpans == 0) return false; 2427 2428 mNumberOfSuggestions = 0; 2429 int spanUnionStart = mTextView.getText().length(); 2430 int spanUnionEnd = 0; 2431 2432 SuggestionSpan misspelledSpan = null; 2433 int underlineColor = 0; 2434 2435 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { 2436 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; 2437 final int spanStart = spannable.getSpanStart(suggestionSpan); 2438 final int spanEnd = spannable.getSpanEnd(suggestionSpan); 2439 spanUnionStart = Math.min(spanStart, spanUnionStart); 2440 spanUnionEnd = Math.max(spanEnd, spanUnionEnd); 2441 2442 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) { 2443 misspelledSpan = suggestionSpan; 2444 } 2445 2446 // The first span dictates the background color of the highlighted text 2447 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor(); 2448 2449 String[] suggestions = suggestionSpan.getSuggestions(); 2450 int nbSuggestions = suggestions.length; 2451 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { 2452 String suggestion = suggestions[suggestionIndex]; 2453 2454 boolean suggestionIsDuplicate = false; 2455 for (int i = 0; i < mNumberOfSuggestions; i++) { 2456 if (mSuggestionInfos[i].text.toString().equals(suggestion)) { 2457 SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan; 2458 final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan); 2459 final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan); 2460 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) { 2461 suggestionIsDuplicate = true; 2462 break; 2463 } 2464 } 2465 } 2466 2467 if (!suggestionIsDuplicate) { 2468 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; 2469 suggestionInfo.suggestionSpan = suggestionSpan; 2470 suggestionInfo.suggestionIndex = suggestionIndex; 2471 suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion); 2472 2473 mNumberOfSuggestions++; 2474 2475 if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) { 2476 // Also end outer for loop 2477 spanIndex = nbSpans; 2478 break; 2479 } 2480 } 2481 } 2482 } 2483 2484 for (int i = 0; i < mNumberOfSuggestions; i++) { 2485 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd); 2486 } 2487 2488 // Add "Add to dictionary" item if there is a span with the misspelled flag 2489 if (misspelledSpan != null) { 2490 final int misspelledStart = spannable.getSpanStart(misspelledSpan); 2491 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan); 2492 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) { 2493 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; 2494 suggestionInfo.suggestionSpan = misspelledSpan; 2495 suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY; 2496 suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView. 2497 getContext().getString(com.android.internal.R.string.addToDictionary)); 2498 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0, 2499 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 2500 2501 mNumberOfSuggestions++; 2502 } 2503 } 2504 2505 // Delete item 2506 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; 2507 suggestionInfo.suggestionSpan = null; 2508 suggestionInfo.suggestionIndex = DELETE_TEXT; 2509 suggestionInfo.text.replace(0, suggestionInfo.text.length(), 2510 mTextView.getContext().getString(com.android.internal.R.string.deleteText)); 2511 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0, 2512 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 2513 mNumberOfSuggestions++; 2514 2515 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); 2516 if (underlineColor == 0) { 2517 // Fallback on the default highlight color when the first span does not provide one 2518 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor); 2519 } else { 2520 final float BACKGROUND_TRANSPARENCY = 0.4f; 2521 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY); 2522 mSuggestionRangeSpan.setBackgroundColor( 2523 (underlineColor & 0x00FFFFFF) + (newAlpha << 24)); 2524 } 2525 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, 2526 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 2527 2528 mSuggestionsAdapter.notifyDataSetChanged(); 2529 return true; 2530 } 2531 2532 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, 2533 int unionEnd) { 2534 final Spannable text = (Spannable) mTextView.getText(); 2535 final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan); 2536 final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan); 2537 2538 // Adjust the start/end of the suggestion span 2539 suggestionInfo.suggestionStart = spanStart - unionStart; 2540 suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart 2541 + suggestionInfo.text.length(); 2542 2543 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 2544 suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 2545 2546 // Add the text before and after the span. 2547 final String textAsString = text.toString(); 2548 suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart)); 2549 suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd)); 2550 } 2551 2552 @Override 2553 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 2554 Editable editable = (Editable) mTextView.getText(); 2555 SuggestionInfo suggestionInfo = mSuggestionInfos[position]; 2556 2557 if (suggestionInfo.suggestionIndex == DELETE_TEXT) { 2558 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan); 2559 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan); 2560 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) { 2561 // Do not leave two adjacent spaces after deletion, or one at beginning of text 2562 if (spanUnionEnd < editable.length() && 2563 Character.isSpaceChar(editable.charAt(spanUnionEnd)) && 2564 (spanUnionStart == 0 || 2565 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) { 2566 spanUnionEnd = spanUnionEnd + 1; 2567 } 2568 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd); 2569 } 2570 hide(); 2571 return; 2572 } 2573 2574 final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan); 2575 final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan); 2576 if (spanStart < 0 || spanEnd <= spanStart) { 2577 // Span has been removed 2578 hide(); 2579 return; 2580 } 2581 2582 final String originalText = editable.toString().substring(spanStart, spanEnd); 2583 2584 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) { 2585 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT); 2586 intent.putExtra("word", originalText); 2587 intent.putExtra("locale", mTextView.getTextServicesLocale().toString()); 2588 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); 2589 mTextView.getContext().startActivity(intent); 2590 // There is no way to know if the word was indeed added. Re-check. 2591 // TODO The ExtractEditText should remove the span in the original text instead 2592 editable.removeSpan(suggestionInfo.suggestionSpan); 2593 Selection.setSelection(editable, spanEnd); 2594 updateSpellCheckSpans(spanStart, spanEnd, false); 2595 } else { 2596 // SuggestionSpans are removed by replace: save them before 2597 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, 2598 SuggestionSpan.class); 2599 final int length = suggestionSpans.length; 2600 int[] suggestionSpansStarts = new int[length]; 2601 int[] suggestionSpansEnds = new int[length]; 2602 int[] suggestionSpansFlags = new int[length]; 2603 for (int i = 0; i < length; i++) { 2604 final SuggestionSpan suggestionSpan = suggestionSpans[i]; 2605 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); 2606 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); 2607 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); 2608 2609 // Remove potential misspelled flags 2610 int suggestionSpanFlags = suggestionSpan.getFlags(); 2611 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) { 2612 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED; 2613 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT; 2614 suggestionSpan.setFlags(suggestionSpanFlags); 2615 } 2616 } 2617 2618 final int suggestionStart = suggestionInfo.suggestionStart; 2619 final int suggestionEnd = suggestionInfo.suggestionEnd; 2620 final String suggestion = suggestionInfo.text.subSequence( 2621 suggestionStart, suggestionEnd).toString(); 2622 mTextView.replaceText_internal(spanStart, spanEnd, suggestion); 2623 2624 // Notify source IME of the suggestion pick. Do this before swaping texts. 2625 if (!TextUtils.isEmpty( 2626 suggestionInfo.suggestionSpan.getNotificationTargetClassName())) { 2627 InputMethodManager imm = InputMethodManager.peekInstance(); 2628 if (imm != null) { 2629 imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText, 2630 suggestionInfo.suggestionIndex); 2631 } 2632 } 2633 2634 // Swap text content between actual text and Suggestion span 2635 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); 2636 suggestions[suggestionInfo.suggestionIndex] = originalText; 2637 2638 // Restore previous SuggestionSpans 2639 final int lengthDifference = suggestion.length() - (spanEnd - spanStart); 2640 for (int i = 0; i < length; i++) { 2641 // Only spans that include the modified region make sense after replacement 2642 // Spans partially included in the replaced region are removed, there is no 2643 // way to assign them a valid range after replacement 2644 if (suggestionSpansStarts[i] <= spanStart && 2645 suggestionSpansEnds[i] >= spanEnd) { 2646 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i], 2647 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]); 2648 } 2649 } 2650 2651 // Move cursor at the end of the replaced word 2652 final int newCursorPosition = spanEnd + lengthDifference; 2653 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition); 2654 } 2655 2656 hide(); 2657 } 2658 } 2659 2660 /** 2661 * An ActionMode Callback class that is used to provide actions while in text selection mode. 2662 * 2663 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending 2664 * on which of these this TextView supports. 2665 */ 2666 private class SelectionActionModeCallback implements ActionMode.Callback { 2667 2668 @Override 2669 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 2670 TypedArray styledAttributes = mTextView.getContext().obtainStyledAttributes( 2671 com.android.internal.R.styleable.SelectionModeDrawables); 2672 2673 boolean allowText = mTextView.getContext().getResources().getBoolean( 2674 com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon); 2675 2676 mode.setTitle(mTextView.getContext().getString( 2677 com.android.internal.R.string.textSelectionCABTitle)); 2678 mode.setSubtitle(null); 2679 mode.setTitleOptionalHint(true); 2680 2681 int selectAllIconId = 0; // No icon by default 2682 if (!allowText) { 2683 // Provide an icon, text will not be displayed on smaller screens. 2684 selectAllIconId = styledAttributes.getResourceId( 2685 R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0); 2686 } 2687 2688 menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). 2689 setIcon(selectAllIconId). 2690 setAlphabeticShortcut('a'). 2691 setShowAsAction( 2692 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); 2693 2694 if (mTextView.canCut()) { 2695 menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut). 2696 setIcon(styledAttributes.getResourceId( 2697 R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)). 2698 setAlphabeticShortcut('x'). 2699 setShowAsAction( 2700 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); 2701 } 2702 2703 if (mTextView.canCopy()) { 2704 menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy). 2705 setIcon(styledAttributes.getResourceId( 2706 R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)). 2707 setAlphabeticShortcut('c'). 2708 setShowAsAction( 2709 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); 2710 } 2711 2712 if (mTextView.canPaste()) { 2713 menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste). 2714 setIcon(styledAttributes.getResourceId( 2715 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)). 2716 setAlphabeticShortcut('v'). 2717 setShowAsAction( 2718 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); 2719 } 2720 2721 styledAttributes.recycle(); 2722 2723 if (mCustomSelectionActionModeCallback != null) { 2724 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) { 2725 // The custom mode can choose to cancel the action mode 2726 return false; 2727 } 2728 } 2729 2730 if (menu.hasVisibleItems() || mode.getCustomView() != null) { 2731 getSelectionController().show(); 2732 mTextView.setHasTransientState(true); 2733 return true; 2734 } else { 2735 return false; 2736 } 2737 } 2738 2739 @Override 2740 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 2741 if (mCustomSelectionActionModeCallback != null) { 2742 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu); 2743 } 2744 return true; 2745 } 2746 2747 @Override 2748 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 2749 if (mCustomSelectionActionModeCallback != null && 2750 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) { 2751 return true; 2752 } 2753 return mTextView.onTextContextMenuItem(item.getItemId()); 2754 } 2755 2756 @Override 2757 public void onDestroyActionMode(ActionMode mode) { 2758 if (mCustomSelectionActionModeCallback != null) { 2759 mCustomSelectionActionModeCallback.onDestroyActionMode(mode); 2760 } 2761 2762 /* 2763 * If we're ending this mode because we're detaching from a window, 2764 * we still have selection state to preserve. Don't clear it, we'll 2765 * bring back the selection mode when (if) we get reattached. 2766 */ 2767 if (!mPreserveDetachedSelection) { 2768 Selection.setSelection((Spannable) mTextView.getText(), 2769 mTextView.getSelectionEnd()); 2770 mTextView.setHasTransientState(false); 2771 } 2772 2773 if (mSelectionModifierCursorController != null) { 2774 mSelectionModifierCursorController.hide(); 2775 } 2776 2777 mSelectionActionMode = null; 2778 } 2779 } 2780 2781 private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener { 2782 private static final int POPUP_TEXT_LAYOUT = 2783 com.android.internal.R.layout.text_edit_action_popup_text; 2784 private TextView mPasteTextView; 2785 private TextView mReplaceTextView; 2786 2787 @Override 2788 protected void createPopupWindow() { 2789 mPopupWindow = new PopupWindow(mTextView.getContext(), null, 2790 com.android.internal.R.attr.textSelectHandleWindowStyle); 2791 mPopupWindow.setClippingEnabled(true); 2792 } 2793 2794 @Override 2795 protected void initContentView() { 2796 LinearLayout linearLayout = new LinearLayout(mTextView.getContext()); 2797 linearLayout.setOrientation(LinearLayout.HORIZONTAL); 2798 mContentView = linearLayout; 2799 mContentView.setBackgroundResource( 2800 com.android.internal.R.drawable.text_edit_paste_window); 2801 2802 LayoutInflater inflater = (LayoutInflater) mTextView.getContext(). 2803 getSystemService(Context.LAYOUT_INFLATER_SERVICE); 2804 2805 LayoutParams wrapContent = new LayoutParams( 2806 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 2807 2808 mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); 2809 mPasteTextView.setLayoutParams(wrapContent); 2810 mContentView.addView(mPasteTextView); 2811 mPasteTextView.setText(com.android.internal.R.string.paste); 2812 mPasteTextView.setOnClickListener(this); 2813 2814 mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); 2815 mReplaceTextView.setLayoutParams(wrapContent); 2816 mContentView.addView(mReplaceTextView); 2817 mReplaceTextView.setText(com.android.internal.R.string.replace); 2818 mReplaceTextView.setOnClickListener(this); 2819 } 2820 2821 @Override 2822 public void show() { 2823 boolean canPaste = mTextView.canPaste(); 2824 boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan(); 2825 mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE); 2826 mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE); 2827 2828 if (!canPaste && !canSuggest) return; 2829 2830 super.show(); 2831 } 2832 2833 @Override 2834 public void onClick(View view) { 2835 if (view == mPasteTextView && mTextView.canPaste()) { 2836 mTextView.onTextContextMenuItem(TextView.ID_PASTE); 2837 hide(); 2838 } else if (view == mReplaceTextView) { 2839 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2; 2840 stopSelectionActionMode(); 2841 Selection.setSelection((Spannable) mTextView.getText(), middle); 2842 showSuggestions(); 2843 } 2844 } 2845 2846 @Override 2847 protected int getTextOffset() { 2848 return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2; 2849 } 2850 2851 @Override 2852 protected int getVerticalLocalPosition(int line) { 2853 return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight(); 2854 } 2855 2856 @Override 2857 protected int clipVertically(int positionY) { 2858 if (positionY < 0) { 2859 final int offset = getTextOffset(); 2860 final Layout layout = mTextView.getLayout(); 2861 final int line = layout.getLineForOffset(offset); 2862 positionY += layout.getLineBottom(line) - layout.getLineTop(line); 2863 positionY += mContentView.getMeasuredHeight(); 2864 2865 // Assumes insertion and selection handles share the same height 2866 final Drawable handle = mTextView.getResources().getDrawable( 2867 mTextView.mTextSelectHandleRes); 2868 positionY += handle.getIntrinsicHeight(); 2869 } 2870 2871 return positionY; 2872 } 2873 } 2874 2875 private abstract class HandleView extends View implements TextViewPositionListener { 2876 protected Drawable mDrawable; 2877 protected Drawable mDrawableLtr; 2878 protected Drawable mDrawableRtl; 2879 private final PopupWindow mContainer; 2880 // Position with respect to the parent TextView 2881 private int mPositionX, mPositionY; 2882 private boolean mIsDragging; 2883 // Offset from touch position to mPosition 2884 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY; 2885 protected int mHotspotX; 2886 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up 2887 private float mTouchOffsetY; 2888 // Where the touch position should be on the handle to ensure a maximum cursor visibility 2889 private float mIdealVerticalOffset; 2890 // Parent's (TextView) previous position in window 2891 private int mLastParentX, mLastParentY; 2892 // Transient action popup window for Paste and Replace actions 2893 protected ActionPopupWindow mActionPopupWindow; 2894 // Previous text character offset 2895 private int mPreviousOffset = -1; 2896 // Previous text character offset 2897 private boolean mPositionHasChanged = true; 2898 // Used to delay the appearance of the action popup window 2899 private Runnable mActionPopupShower; 2900 2901 public HandleView(Drawable drawableLtr, Drawable drawableRtl) { 2902 super(mTextView.getContext()); 2903 mContainer = new PopupWindow(mTextView.getContext(), null, 2904 com.android.internal.R.attr.textSelectHandleWindowStyle); 2905 mContainer.setSplitTouchEnabled(true); 2906 mContainer.setClippingEnabled(false); 2907 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); 2908 mContainer.setContentView(this); 2909 2910 mDrawableLtr = drawableLtr; 2911 mDrawableRtl = drawableRtl; 2912 2913 updateDrawable(); 2914 2915 final int handleHeight = mDrawable.getIntrinsicHeight(); 2916 mTouchOffsetY = -0.3f * handleHeight; 2917 mIdealVerticalOffset = 0.7f * handleHeight; 2918 } 2919 2920 protected void updateDrawable() { 2921 final int offset = getCurrentCursorOffset(); 2922 final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset); 2923 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr; 2924 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset); 2925 } 2926 2927 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun); 2928 2929 // Touch-up filter: number of previous positions remembered 2930 private static final int HISTORY_SIZE = 5; 2931 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150; 2932 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350; 2933 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE]; 2934 private final int[] mPreviousOffsets = new int[HISTORY_SIZE]; 2935 private int mPreviousOffsetIndex = 0; 2936 private int mNumberPreviousOffsets = 0; 2937 2938 private void startTouchUpFilter(int offset) { 2939 mNumberPreviousOffsets = 0; 2940 addPositionToTouchUpFilter(offset); 2941 } 2942 2943 private void addPositionToTouchUpFilter(int offset) { 2944 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE; 2945 mPreviousOffsets[mPreviousOffsetIndex] = offset; 2946 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis(); 2947 mNumberPreviousOffsets++; 2948 } 2949 2950 private void filterOnTouchUp() { 2951 final long now = SystemClock.uptimeMillis(); 2952 int i = 0; 2953 int index = mPreviousOffsetIndex; 2954 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE); 2955 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) { 2956 i++; 2957 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE; 2958 } 2959 2960 if (i > 0 && i < iMax && 2961 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) { 2962 positionAtCursorOffset(mPreviousOffsets[index], false); 2963 } 2964 } 2965 2966 public boolean offsetHasBeenChanged() { 2967 return mNumberPreviousOffsets > 1; 2968 } 2969 2970 @Override 2971 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 2972 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); 2973 } 2974 2975 public void show() { 2976 if (isShowing()) return; 2977 2978 getPositionListener().addSubscriber(this, true /* local position may change */); 2979 2980 // Make sure the offset is always considered new, even when focusing at same position 2981 mPreviousOffset = -1; 2982 positionAtCursorOffset(getCurrentCursorOffset(), false); 2983 2984 hideActionPopupWindow(); 2985 } 2986 2987 protected void dismiss() { 2988 mIsDragging = false; 2989 mContainer.dismiss(); 2990 onDetached(); 2991 } 2992 2993 public void hide() { 2994 dismiss(); 2995 2996 getPositionListener().removeSubscriber(this); 2997 } 2998 2999 void showActionPopupWindow(int delay) { 3000 if (mActionPopupWindow == null) { 3001 mActionPopupWindow = new ActionPopupWindow(); 3002 } 3003 if (mActionPopupShower == null) { 3004 mActionPopupShower = new Runnable() { 3005 public void run() { 3006 mActionPopupWindow.show(); 3007 } 3008 }; 3009 } else { 3010 mTextView.removeCallbacks(mActionPopupShower); 3011 } 3012 mTextView.postDelayed(mActionPopupShower, delay); 3013 } 3014 3015 protected void hideActionPopupWindow() { 3016 if (mActionPopupShower != null) { 3017 mTextView.removeCallbacks(mActionPopupShower); 3018 } 3019 if (mActionPopupWindow != null) { 3020 mActionPopupWindow.hide(); 3021 } 3022 } 3023 3024 public boolean isShowing() { 3025 return mContainer.isShowing(); 3026 } 3027 3028 private boolean isVisible() { 3029 // Always show a dragging handle. 3030 if (mIsDragging) { 3031 return true; 3032 } 3033 3034 if (mTextView.isInBatchEditMode()) { 3035 return false; 3036 } 3037 3038 return isPositionVisible(mPositionX + mHotspotX, mPositionY); 3039 } 3040 3041 public abstract int getCurrentCursorOffset(); 3042 3043 protected abstract void updateSelection(int offset); 3044 3045 public abstract void updatePosition(float x, float y); 3046 3047 protected void positionAtCursorOffset(int offset, boolean parentScrolled) { 3048 // A HandleView relies on the layout, which may be nulled by external methods 3049 Layout layout = mTextView.getLayout(); 3050 if (layout == null) { 3051 // Will update controllers' state, hiding them and stopping selection mode if needed 3052 prepareCursorControllers(); 3053 return; 3054 } 3055 3056 boolean offsetChanged = offset != mPreviousOffset; 3057 if (offsetChanged || parentScrolled) { 3058 if (offsetChanged) { 3059 updateSelection(offset); 3060 addPositionToTouchUpFilter(offset); 3061 } 3062 final int line = layout.getLineForOffset(offset); 3063 3064 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX); 3065 mPositionY = layout.getLineBottom(line); 3066 3067 // Take TextView's padding and scroll into account. 3068 mPositionX += mTextView.viewportToContentHorizontalOffset(); 3069 mPositionY += mTextView.viewportToContentVerticalOffset(); 3070 3071 mPreviousOffset = offset; 3072 mPositionHasChanged = true; 3073 } 3074 } 3075 3076 public void updatePosition(int parentPositionX, int parentPositionY, 3077 boolean parentPositionChanged, boolean parentScrolled) { 3078 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled); 3079 if (parentPositionChanged || mPositionHasChanged) { 3080 if (mIsDragging) { 3081 // Update touchToWindow offset in case of parent scrolling while dragging 3082 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) { 3083 mTouchToWindowOffsetX += parentPositionX - mLastParentX; 3084 mTouchToWindowOffsetY += parentPositionY - mLastParentY; 3085 mLastParentX = parentPositionX; 3086 mLastParentY = parentPositionY; 3087 } 3088 3089 onHandleMoved(); 3090 } 3091 3092 if (isVisible()) { 3093 final int positionX = parentPositionX + mPositionX; 3094 final int positionY = parentPositionY + mPositionY; 3095 if (isShowing()) { 3096 mContainer.update(positionX, positionY, -1, -1); 3097 } else { 3098 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, 3099 positionX, positionY); 3100 } 3101 } else { 3102 if (isShowing()) { 3103 dismiss(); 3104 } 3105 } 3106 3107 mPositionHasChanged = false; 3108 } 3109 } 3110 3111 @Override 3112 protected void onDraw(Canvas c) { 3113 mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop); 3114 mDrawable.draw(c); 3115 } 3116 3117 @Override 3118 public boolean onTouchEvent(MotionEvent ev) { 3119 switch (ev.getActionMasked()) { 3120 case MotionEvent.ACTION_DOWN: { 3121 startTouchUpFilter(getCurrentCursorOffset()); 3122 mTouchToWindowOffsetX = ev.getRawX() - mPositionX; 3123 mTouchToWindowOffsetY = ev.getRawY() - mPositionY; 3124 3125 final PositionListener positionListener = getPositionListener(); 3126 mLastParentX = positionListener.getPositionX(); 3127 mLastParentY = positionListener.getPositionY(); 3128 mIsDragging = true; 3129 break; 3130 } 3131 3132 case MotionEvent.ACTION_MOVE: { 3133 final float rawX = ev.getRawX(); 3134 final float rawY = ev.getRawY(); 3135 3136 // Vertical hysteresis: vertical down movement tends to snap to ideal offset 3137 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY; 3138 final float currentVerticalOffset = rawY - mPositionY - mLastParentY; 3139 float newVerticalOffset; 3140 if (previousVerticalOffset < mIdealVerticalOffset) { 3141 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset); 3142 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset); 3143 } else { 3144 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset); 3145 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset); 3146 } 3147 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY; 3148 3149 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; 3150 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY; 3151 3152 updatePosition(newPosX, newPosY); 3153 break; 3154 } 3155 3156 case MotionEvent.ACTION_UP: 3157 filterOnTouchUp(); 3158 mIsDragging = false; 3159 break; 3160 3161 case MotionEvent.ACTION_CANCEL: 3162 mIsDragging = false; 3163 break; 3164 } 3165 return true; 3166 } 3167 3168 public boolean isDragging() { 3169 return mIsDragging; 3170 } 3171 3172 void onHandleMoved() { 3173 hideActionPopupWindow(); 3174 } 3175 3176 public void onDetached() { 3177 hideActionPopupWindow(); 3178 } 3179 } 3180 3181 private class InsertionHandleView extends HandleView { 3182 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000; 3183 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds 3184 3185 // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow 3186 private float mDownPositionX, mDownPositionY; 3187 private Runnable mHider; 3188 3189 public InsertionHandleView(Drawable drawable) { 3190 super(drawable, drawable); 3191 } 3192 3193 @Override 3194 public void show() { 3195 super.show(); 3196 3197 final long durationSinceCutOrCopy = 3198 SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME; 3199 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) { 3200 showActionPopupWindow(0); 3201 } 3202 3203 hideAfterDelay(); 3204 } 3205 3206 public void showWithActionPopup() { 3207 show(); 3208 showActionPopupWindow(0); 3209 } 3210 3211 private void hideAfterDelay() { 3212 if (mHider == null) { 3213 mHider = new Runnable() { 3214 public void run() { 3215 hide(); 3216 } 3217 }; 3218 } else { 3219 removeHiderCallback(); 3220 } 3221 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT); 3222 } 3223 3224 private void removeHiderCallback() { 3225 if (mHider != null) { 3226 mTextView.removeCallbacks(mHider); 3227 } 3228 } 3229 3230 @Override 3231 protected int getHotspotX(Drawable drawable, boolean isRtlRun) { 3232 return drawable.getIntrinsicWidth() / 2; 3233 } 3234 3235 @Override 3236 public boolean onTouchEvent(MotionEvent ev) { 3237 final boolean result = super.onTouchEvent(ev); 3238 3239 switch (ev.getActionMasked()) { 3240 case MotionEvent.ACTION_DOWN: 3241 mDownPositionX = ev.getRawX(); 3242 mDownPositionY = ev.getRawY(); 3243 break; 3244 3245 case MotionEvent.ACTION_UP: 3246 if (!offsetHasBeenChanged()) { 3247 final float deltaX = mDownPositionX - ev.getRawX(); 3248 final float deltaY = mDownPositionY - ev.getRawY(); 3249 final float distanceSquared = deltaX * deltaX + deltaY * deltaY; 3250 3251 final ViewConfiguration viewConfiguration = ViewConfiguration.get( 3252 mTextView.getContext()); 3253 final int touchSlop = viewConfiguration.getScaledTouchSlop(); 3254 3255 if (distanceSquared < touchSlop * touchSlop) { 3256 if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) { 3257 // Tapping on the handle dismisses the displayed action popup 3258 mActionPopupWindow.hide(); 3259 } else { 3260 showWithActionPopup(); 3261 } 3262 } 3263 } 3264 hideAfterDelay(); 3265 break; 3266 3267 case MotionEvent.ACTION_CANCEL: 3268 hideAfterDelay(); 3269 break; 3270 3271 default: 3272 break; 3273 } 3274 3275 return result; 3276 } 3277 3278 @Override 3279 public int getCurrentCursorOffset() { 3280 return mTextView.getSelectionStart(); 3281 } 3282 3283 @Override 3284 public void updateSelection(int offset) { 3285 Selection.setSelection((Spannable) mTextView.getText(), offset); 3286 } 3287 3288 @Override 3289 public void updatePosition(float x, float y) { 3290 positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false); 3291 } 3292 3293 @Override 3294 void onHandleMoved() { 3295 super.onHandleMoved(); 3296 removeHiderCallback(); 3297 } 3298 3299 @Override 3300 public void onDetached() { 3301 super.onDetached(); 3302 removeHiderCallback(); 3303 } 3304 } 3305 3306 private class SelectionStartHandleView extends HandleView { 3307 3308 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) { 3309 super(drawableLtr, drawableRtl); 3310 } 3311 3312 @Override 3313 protected int getHotspotX(Drawable drawable, boolean isRtlRun) { 3314 if (isRtlRun) { 3315 return drawable.getIntrinsicWidth() / 4; 3316 } else { 3317 return (drawable.getIntrinsicWidth() * 3) / 4; 3318 } 3319 } 3320 3321 @Override 3322 public int getCurrentCursorOffset() { 3323 return mTextView.getSelectionStart(); 3324 } 3325 3326 @Override 3327 public void updateSelection(int offset) { 3328 Selection.setSelection((Spannable) mTextView.getText(), offset, 3329 mTextView.getSelectionEnd()); 3330 updateDrawable(); 3331 } 3332 3333 @Override 3334 public void updatePosition(float x, float y) { 3335 int offset = mTextView.getOffsetForPosition(x, y); 3336 3337 // Handles can not cross and selection is at least one character 3338 final int selectionEnd = mTextView.getSelectionEnd(); 3339 if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1); 3340 3341 positionAtCursorOffset(offset, false); 3342 } 3343 3344 public ActionPopupWindow getActionPopupWindow() { 3345 return mActionPopupWindow; 3346 } 3347 } 3348 3349 private class SelectionEndHandleView extends HandleView { 3350 3351 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) { 3352 super(drawableLtr, drawableRtl); 3353 } 3354 3355 @Override 3356 protected int getHotspotX(Drawable drawable, boolean isRtlRun) { 3357 if (isRtlRun) { 3358 return (drawable.getIntrinsicWidth() * 3) / 4; 3359 } else { 3360 return drawable.getIntrinsicWidth() / 4; 3361 } 3362 } 3363 3364 @Override 3365 public int getCurrentCursorOffset() { 3366 return mTextView.getSelectionEnd(); 3367 } 3368 3369 @Override 3370 public void updateSelection(int offset) { 3371 Selection.setSelection((Spannable) mTextView.getText(), 3372 mTextView.getSelectionStart(), offset); 3373 updateDrawable(); 3374 } 3375 3376 @Override 3377 public void updatePosition(float x, float y) { 3378 int offset = mTextView.getOffsetForPosition(x, y); 3379 3380 // Handles can not cross and selection is at least one character 3381 final int selectionStart = mTextView.getSelectionStart(); 3382 if (offset <= selectionStart) { 3383 offset = Math.min(selectionStart + 1, mTextView.getText().length()); 3384 } 3385 3386 positionAtCursorOffset(offset, false); 3387 } 3388 3389 public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) { 3390 mActionPopupWindow = actionPopupWindow; 3391 } 3392 } 3393 3394 /** 3395 * A CursorController instance can be used to control a cursor in the text. 3396 */ 3397 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { 3398 /** 3399 * Makes the cursor controller visible on screen. 3400 * See also {@link #hide()}. 3401 */ 3402 public void show(); 3403 3404 /** 3405 * Hide the cursor controller from screen. 3406 * See also {@link #show()}. 3407 */ 3408 public void hide(); 3409 3410 /** 3411 * Called when the view is detached from window. Perform house keeping task, such as 3412 * stopping Runnable thread that would otherwise keep a reference on the context, thus 3413 * preventing the activity from being recycled. 3414 */ 3415 public void onDetached(); 3416 } 3417 3418 private class InsertionPointCursorController implements CursorController { 3419 private InsertionHandleView mHandle; 3420 3421 public void show() { 3422 getHandle().show(); 3423 } 3424 3425 public void showWithActionPopup() { 3426 getHandle().showWithActionPopup(); 3427 } 3428 3429 public void hide() { 3430 if (mHandle != null) { 3431 mHandle.hide(); 3432 } 3433 } 3434 3435 public void onTouchModeChanged(boolean isInTouchMode) { 3436 if (!isInTouchMode) { 3437 hide(); 3438 } 3439 } 3440 3441 private InsertionHandleView getHandle() { 3442 if (mSelectHandleCenter == null) { 3443 mSelectHandleCenter = mTextView.getResources().getDrawable( 3444 mTextView.mTextSelectHandleRes); 3445 } 3446 if (mHandle == null) { 3447 mHandle = new InsertionHandleView(mSelectHandleCenter); 3448 } 3449 return mHandle; 3450 } 3451 3452 @Override 3453 public void onDetached() { 3454 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 3455 observer.removeOnTouchModeChangeListener(this); 3456 3457 if (mHandle != null) mHandle.onDetached(); 3458 } 3459 } 3460 3461 class SelectionModifierCursorController implements CursorController { 3462 private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds 3463 // The cursor controller handles, lazily created when shown. 3464 private SelectionStartHandleView mStartHandle; 3465 private SelectionEndHandleView mEndHandle; 3466 // The offsets of that last touch down event. Remembered to start selection there. 3467 private int mMinTouchOffset, mMaxTouchOffset; 3468 3469 // Double tap detection 3470 private long mPreviousTapUpTime = 0; 3471 private float mDownPositionX, mDownPositionY; 3472 private boolean mGestureStayedInTapRegion; 3473 3474 SelectionModifierCursorController() { 3475 resetTouchOffsets(); 3476 } 3477 3478 public void show() { 3479 if (mTextView.isInBatchEditMode()) { 3480 return; 3481 } 3482 initDrawables(); 3483 initHandles(); 3484 hideInsertionPointCursorController(); 3485 } 3486 3487 private void initDrawables() { 3488 if (mSelectHandleLeft == null) { 3489 mSelectHandleLeft = mTextView.getContext().getResources().getDrawable( 3490 mTextView.mTextSelectHandleLeftRes); 3491 } 3492 if (mSelectHandleRight == null) { 3493 mSelectHandleRight = mTextView.getContext().getResources().getDrawable( 3494 mTextView.mTextSelectHandleRightRes); 3495 } 3496 } 3497 3498 private void initHandles() { 3499 // Lazy object creation has to be done before updatePosition() is called. 3500 if (mStartHandle == null) { 3501 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight); 3502 } 3503 if (mEndHandle == null) { 3504 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft); 3505 } 3506 3507 mStartHandle.show(); 3508 mEndHandle.show(); 3509 3510 // Make sure both left and right handles share the same ActionPopupWindow (so that 3511 // moving any of the handles hides the action popup). 3512 mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION); 3513 mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow()); 3514 3515 hideInsertionPointCursorController(); 3516 } 3517 3518 public void hide() { 3519 if (mStartHandle != null) mStartHandle.hide(); 3520 if (mEndHandle != null) mEndHandle.hide(); 3521 } 3522 3523 public void onTouchEvent(MotionEvent event) { 3524 // This is done even when the View does not have focus, so that long presses can start 3525 // selection and tap can move cursor from this tap position. 3526 switch (event.getActionMasked()) { 3527 case MotionEvent.ACTION_DOWN: 3528 final float x = event.getX(); 3529 final float y = event.getY(); 3530 3531 // Remember finger down position, to be able to start selection from there 3532 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y); 3533 3534 // Double tap detection 3535 if (mGestureStayedInTapRegion) { 3536 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; 3537 if (duration <= ViewConfiguration.getDoubleTapTimeout()) { 3538 final float deltaX = x - mDownPositionX; 3539 final float deltaY = y - mDownPositionY; 3540 final float distanceSquared = deltaX * deltaX + deltaY * deltaY; 3541 3542 ViewConfiguration viewConfiguration = ViewConfiguration.get( 3543 mTextView.getContext()); 3544 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop(); 3545 boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop; 3546 3547 if (stayedInArea && isPositionOnText(x, y)) { 3548 startSelectionActionMode(); 3549 mDiscardNextActionUp = true; 3550 } 3551 } 3552 } 3553 3554 mDownPositionX = x; 3555 mDownPositionY = y; 3556 mGestureStayedInTapRegion = true; 3557 break; 3558 3559 case MotionEvent.ACTION_POINTER_DOWN: 3560 case MotionEvent.ACTION_POINTER_UP: 3561 // Handle multi-point gestures. Keep min and max offset positions. 3562 // Only activated for devices that correctly handle multi-touch. 3563 if (mTextView.getContext().getPackageManager().hasSystemFeature( 3564 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { 3565 updateMinAndMaxOffsets(event); 3566 } 3567 break; 3568 3569 case MotionEvent.ACTION_MOVE: 3570 if (mGestureStayedInTapRegion) { 3571 final float deltaX = event.getX() - mDownPositionX; 3572 final float deltaY = event.getY() - mDownPositionY; 3573 final float distanceSquared = deltaX * deltaX + deltaY * deltaY; 3574 3575 final ViewConfiguration viewConfiguration = ViewConfiguration.get( 3576 mTextView.getContext()); 3577 int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop(); 3578 3579 if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) { 3580 mGestureStayedInTapRegion = false; 3581 } 3582 } 3583 break; 3584 3585 case MotionEvent.ACTION_UP: 3586 mPreviousTapUpTime = SystemClock.uptimeMillis(); 3587 break; 3588 } 3589 } 3590 3591 /** 3592 * @param event 3593 */ 3594 private void updateMinAndMaxOffsets(MotionEvent event) { 3595 int pointerCount = event.getPointerCount(); 3596 for (int index = 0; index < pointerCount; index++) { 3597 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index)); 3598 if (offset < mMinTouchOffset) mMinTouchOffset = offset; 3599 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; 3600 } 3601 } 3602 3603 public int getMinTouchOffset() { 3604 return mMinTouchOffset; 3605 } 3606 3607 public int getMaxTouchOffset() { 3608 return mMaxTouchOffset; 3609 } 3610 3611 public void resetTouchOffsets() { 3612 mMinTouchOffset = mMaxTouchOffset = -1; 3613 } 3614 3615 /** 3616 * @return true iff this controller is currently used to move the selection start. 3617 */ 3618 public boolean isSelectionStartDragged() { 3619 return mStartHandle != null && mStartHandle.isDragging(); 3620 } 3621 3622 public void onTouchModeChanged(boolean isInTouchMode) { 3623 if (!isInTouchMode) { 3624 hide(); 3625 } 3626 } 3627 3628 @Override 3629 public void onDetached() { 3630 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 3631 observer.removeOnTouchModeChangeListener(this); 3632 3633 if (mStartHandle != null) mStartHandle.onDetached(); 3634 if (mEndHandle != null) mEndHandle.onDetached(); 3635 } 3636 } 3637 3638 private class CorrectionHighlighter { 3639 private final Path mPath = new Path(); 3640 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 3641 private int mStart, mEnd; 3642 private long mFadingStartTime; 3643 private RectF mTempRectF; 3644 private final static int FADE_OUT_DURATION = 400; 3645 3646 public CorrectionHighlighter() { 3647 mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo(). 3648 applicationScale); 3649 mPaint.setStyle(Paint.Style.FILL); 3650 } 3651 3652 public void highlight(CorrectionInfo info) { 3653 mStart = info.getOffset(); 3654 mEnd = mStart + info.getNewText().length(); 3655 mFadingStartTime = SystemClock.uptimeMillis(); 3656 3657 if (mStart < 0 || mEnd < 0) { 3658 stopAnimation(); 3659 } 3660 } 3661 3662 public void draw(Canvas canvas, int cursorOffsetVertical) { 3663 if (updatePath() && updatePaint()) { 3664 if (cursorOffsetVertical != 0) { 3665 canvas.translate(0, cursorOffsetVertical); 3666 } 3667 3668 canvas.drawPath(mPath, mPaint); 3669 3670 if (cursorOffsetVertical != 0) { 3671 canvas.translate(0, -cursorOffsetVertical); 3672 } 3673 invalidate(true); // TODO invalidate cursor region only 3674 } else { 3675 stopAnimation(); 3676 invalidate(false); // TODO invalidate cursor region only 3677 } 3678 } 3679 3680 private boolean updatePaint() { 3681 final long duration = SystemClock.uptimeMillis() - mFadingStartTime; 3682 if (duration > FADE_OUT_DURATION) return false; 3683 3684 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION; 3685 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor); 3686 final int color = (mTextView.mHighlightColor & 0x00FFFFFF) + 3687 ((int) (highlightColorAlpha * coef) << 24); 3688 mPaint.setColor(color); 3689 return true; 3690 } 3691 3692 private boolean updatePath() { 3693 final Layout layout = mTextView.getLayout(); 3694 if (layout == null) return false; 3695 3696 // Update in case text is edited while the animation is run 3697 final int length = mTextView.getText().length(); 3698 int start = Math.min(length, mStart); 3699 int end = Math.min(length, mEnd); 3700 3701 mPath.reset(); 3702 layout.getSelectionPath(start, end, mPath); 3703 return true; 3704 } 3705 3706 private void invalidate(boolean delayed) { 3707 if (mTextView.getLayout() == null) return; 3708 3709 if (mTempRectF == null) mTempRectF = new RectF(); 3710 mPath.computeBounds(mTempRectF, false); 3711 3712 int left = mTextView.getCompoundPaddingLeft(); 3713 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true); 3714 3715 if (delayed) { 3716 mTextView.postInvalidateOnAnimation( 3717 left + (int) mTempRectF.left, top + (int) mTempRectF.top, 3718 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom); 3719 } else { 3720 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top, 3721 (int) mTempRectF.right, (int) mTempRectF.bottom); 3722 } 3723 } 3724 3725 private void stopAnimation() { 3726 Editor.this.mCorrectionHighlighter = null; 3727 } 3728 } 3729 3730 private static class ErrorPopup extends PopupWindow { 3731 private boolean mAbove = false; 3732 private final TextView mView; 3733 private int mPopupInlineErrorBackgroundId = 0; 3734 private int mPopupInlineErrorAboveBackgroundId = 0; 3735 3736 ErrorPopup(TextView v, int width, int height) { 3737 super(v, width, height); 3738 mView = v; 3739 // Make sure the TextView has a background set as it will be used the first time it is 3740 // shown and positionned. Initialized with below background, which should have 3741 // dimensions identical to the above version for this to work (and is more likely). 3742 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, 3743 com.android.internal.R.styleable.Theme_errorMessageBackground); 3744 mView.setBackgroundResource(mPopupInlineErrorBackgroundId); 3745 } 3746 3747 void fixDirection(boolean above) { 3748 mAbove = above; 3749 3750 if (above) { 3751 mPopupInlineErrorAboveBackgroundId = 3752 getResourceId(mPopupInlineErrorAboveBackgroundId, 3753 com.android.internal.R.styleable.Theme_errorMessageAboveBackground); 3754 } else { 3755 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, 3756 com.android.internal.R.styleable.Theme_errorMessageBackground); 3757 } 3758 3759 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId : 3760 mPopupInlineErrorBackgroundId); 3761 } 3762 3763 private int getResourceId(int currentId, int index) { 3764 if (currentId == 0) { 3765 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes( 3766 R.styleable.Theme); 3767 currentId = styledAttributes.getResourceId(index, 0); 3768 styledAttributes.recycle(); 3769 } 3770 return currentId; 3771 } 3772 3773 @Override 3774 public void update(int x, int y, int w, int h, boolean force) { 3775 super.update(x, y, w, h, force); 3776 3777 boolean above = isAboveAnchor(); 3778 if (above != mAbove) { 3779 fixDirection(above); 3780 } 3781 } 3782 } 3783 3784 static class InputContentType { 3785 int imeOptions = EditorInfo.IME_NULL; 3786 String privateImeOptions; 3787 CharSequence imeActionLabel; 3788 int imeActionId; 3789 Bundle extras; 3790 OnEditorActionListener onEditorActionListener; 3791 boolean enterDown; 3792 } 3793 3794 static class InputMethodState { 3795 Rect mCursorRectInWindow = new Rect(); 3796 RectF mTmpRectF = new RectF(); 3797 float[] mTmpOffset = new float[2]; 3798 ExtractedTextRequest mExtractedTextRequest; 3799 final ExtractedText mExtractedText = new ExtractedText(); 3800 int mBatchEditNesting; 3801 boolean mCursorChanged; 3802 boolean mSelectionModeChanged; 3803 boolean mContentChanged; 3804 int mChangedStart, mChangedEnd, mChangedDelta; 3805 } 3806} 3807