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