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