From 83b68ba4ad7e80dd0afe34f9f53dde5884325f58 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Mon, 12 May 2014 15:46:25 +0900 Subject: [PATCH] Support IMM#updateCursorAnchorInfo in Editor With this CL, Editor starts calling IMM#updateCursorAnchorInfo iff IMM#isWatchingCursor(View) returns true. The performance regression should be negligible when IMM#isWatchingCursor(View) returns false. BUG: 14579622 Change-Id: Ib35a6f4201b26d837a6aa07d1204c9eb75404da1 --- core/java/android/widget/Editor.java | 124 ++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 2 deletions(-) diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index b0a4e24803a87..cbe7511f918fe 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -39,6 +39,7 @@ import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; @@ -94,6 +95,8 @@ import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -215,6 +218,8 @@ public class Editor { private TextView mTextView; + final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier = new CursorAnchorInfoNotifier(); + Editor(TextView textView) { mTextView = textView; } @@ -249,9 +254,13 @@ public class Editor { // We had an active selection from before, start the selection mode. startSelectionActionMode(); } + + getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); } void onDetachedFromWindow() { + getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier); + if (mError != null) { hideError(); } @@ -780,7 +789,7 @@ public class Editor { boolean parentPositionChanged, boolean parentScrolled); } - private boolean isPositionVisible(int positionX, int positionY) { + private boolean isPositionVisible(final float positionX, final float positionY) { synchronized (TEMP_POSITION) { final float[] position = TEMP_POSITION; position[0] = positionX; @@ -2134,7 +2143,8 @@ public class Editor { private class PositionListener implements ViewTreeObserver.OnPreDrawListener { // 3 handles // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others) - private final int MAXIMUM_NUMBER_OF_LISTENERS = 6; + // 1 CursorAnchorInfoNotifier + private final int MAXIMUM_NUMBER_OF_LISTENERS = 7; private TextViewPositionListener[] mPositionListeners = new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS]; private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS]; @@ -2997,6 +3007,116 @@ public class Editor { } } + /** + * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} + * while the input method is requesting the cursor/anchor position. Does nothing as long as + * {@link InputMethodManager#isWatchingCursor(View)} returns false. + */ + private final class CursorAnchorInfoNotifier implements TextViewPositionListener { + final CursorAnchorInfoBuilder mSelectionInfoBuilder = new CursorAnchorInfoBuilder(); + final int[] mTmpIntOffset = new int[2]; + final Matrix mViewToScreenMatrix = new Matrix(); + + @Override + public void updatePosition(int parentPositionX, int parentPositionY, + boolean parentPositionChanged, boolean parentScrolled) { + final InputMethodState ims = mInputMethodState; + if (ims == null || ims.mBatchEditNesting > 0) { + return; + } + final InputMethodManager imm = InputMethodManager.peekInstance(); + if (null == imm) { + return; + } + // Skip if the IME has not requested the cursor/anchor position. + if (!imm.isWatchingCursor(mTextView)) { + return; + } + Layout layout = mTextView.getLayout(); + if (layout == null) { + return; + } + + final CursorAnchorInfoBuilder builder = mSelectionInfoBuilder; + builder.reset(); + + final int selectionStart = mTextView.getSelectionStart(); + final int selectionEnd = mTextView.getSelectionEnd(); + builder.setSelectionRange(mTextView.getSelectionStart(), mTextView.getSelectionEnd()); + + // Construct transformation matrix from view local coordinates to screen coordinates. + mViewToScreenMatrix.set(mTextView.getMatrix()); + mTextView.getLocationOnScreen(mTmpIntOffset); + mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]); + builder.setMatrix(mViewToScreenMatrix); + + final float viewportToContentHorizontalOffset = + mTextView.viewportToContentHorizontalOffset(); + final float viewportToContentVerticalOffset = + mTextView.viewportToContentVerticalOffset(); + + if (mTextView.getText() instanceof Spannable) { + final Spannable sp = (Spannable) mTextView.getText(); + int compositionStart = EditableInputConnection.getComposingSpanStart(sp); + int compositionEnd = EditableInputConnection.getComposingSpanEnd(sp); + if (compositionEnd < compositionStart) { + final int temp = compositionEnd; + compositionEnd = compositionStart; + compositionStart = temp; + } + builder.setCandidateRange(compositionStart, compositionEnd); + for (int offset = compositionStart; offset < compositionEnd; offset++) { + if (offset < 0) { + continue; + } + final int line = layout.getLineForOffset(offset); + final float left = layout.getPrimaryHorizontal(offset) + + viewportToContentHorizontalOffset; + final float top = layout.getLineTop(line) + viewportToContentVerticalOffset; + // Here we are tentatively passing offset + 1 to calculate the other side of + // the primary horizontal to preserve as many positions as possible so that + // the IME can reconstruct the layout entirely. However, we should revisit this + // to have a clear specification about the relationship between the index of + // the character and its bounding box. See also the TODO comment below. + final float right = layout.getPrimaryHorizontal(offset + 1) + + viewportToContentHorizontalOffset; + final float bottom = layout.getLineBottom(line) + + viewportToContentVerticalOffset; + // Take TextView's padding and scroll into account. + if (isPositionVisible(left, top) && isPositionVisible(right, bottom)) { + // Here offset is the index in Java chars. + // TODO: We must have a well-defined specification. For example, how + // RTL, surrogate pairs, and composition letters are handled must be + // documented. + builder.addCharacterRect(offset, left, top, right, bottom); + } + } + } + + // Treat selectionStart as the insertion point. + if (0 <= selectionStart) { + final int offset = selectionStart; + final int line = layout.getLineForOffset(offset); + final float insertionMarkerX = layout.getPrimaryHorizontal(offset) + + viewportToContentHorizontalOffset; + final float insertionMarkerTop = layout.getLineTop(line) + + viewportToContentVerticalOffset; + final float insertionMarkerBaseline = layout.getLineBaseline(line) + + viewportToContentVerticalOffset; + final float insertionMarkerBottom = layout.getLineBottom(line) + + viewportToContentVerticalOffset; + // Take TextView's padding and scroll into account. + if (isPositionVisible(insertionMarkerX, insertionMarkerTop) && + isPositionVisible(insertionMarkerX, insertionMarkerBottom)) { + builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop, + insertionMarkerBaseline, insertionMarkerBottom); + } + } + + imm.updateCursorAnchorInfo(mTextView, builder.build()); + } + } + private abstract class HandleView extends View implements TextViewPositionListener { protected Drawable mDrawable; protected Drawable mDrawableLtr;