diff options
| -rw-r--r-- | core/java/android/widget/Editor.java | 59 | ||||
| -rw-r--r-- | core/java/android/widget/TextView.java | 70 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java | 98 |
3 files changed, 189 insertions, 38 deletions
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index d37c37a392a5..3da9e96618f3 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -8105,6 +8105,16 @@ public class Editor { private final Paint mHighlightPaint; private final Path mHighlightPath; + /** + * Whether it is in the progress of updating transformation method. It's needed because + * {@link TextView#setTransformationMethod(TransformationMethod)} will eventually call + * {@link TextView#setText(CharSequence)}. + * Because it normally should exit insert mode when {@link TextView#setText(CharSequence)} + * is called externally, we need this boolean to distinguish whether setText is triggered + * by setTransformation or not. + */ + private boolean mUpdatingTransformationMethod; + InsertModeController(@NonNull TextView textView) { mTextView = Objects.requireNonNull(textView); mIsInsertModeActive = false; @@ -8137,7 +8147,7 @@ public class Editor { final boolean isSingleLine = mTextView.isSingleLine(); mInsertModeTransformationMethod = new InsertModeTransformationMethod(offset, isSingleLine, oldTransformationMethod); - mTextView.setTransformationMethodInternal(mInsertModeTransformationMethod); + setTransformationMethod(mInsertModeTransformationMethod, true); Selection.setSelection((Spannable) mTextView.getText(), offset); mIsInsertModeActive = true; @@ -8145,6 +8155,10 @@ public class Editor { } void exitInsertMode() { + exitInsertMode(true); + } + + void exitInsertMode(boolean updateText) { if (!mIsInsertModeActive) return; if (mInsertModeTransformationMethod == null || mInsertModeTransformationMethod != mTextView.getTransformationMethod()) { @@ -8157,7 +8171,7 @@ public class Editor { final int selectionEnd = mTextView.getSelectionEnd(); final TransformationMethod oldTransformationMethod = mInsertModeTransformationMethod.getOldTransformationMethod(); - mTextView.setTransformationMethodInternal(oldTransformationMethod); + setTransformationMethod(oldTransformationMethod, updateText); Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); mIsInsertModeActive = false; } @@ -8178,6 +8192,32 @@ public class Editor { } /** + * Update the TransformationMethod on the {@link TextView}. + * @param method the new method to be set on the {@link TextView}/ + * @param updateText whether to update the text during setTransformationMethod call. + */ + private void setTransformationMethod(TransformationMethod method, boolean updateText) { + mUpdatingTransformationMethod = true; + mTextView.setTransformationMethodInternal(method, updateText); + mUpdatingTransformationMethod = false; + } + + /** + * Notify the InsertMode controller that the {@link TextView} is about to set its text. + */ + void beforeSetText() { + // TextView#setText is called because our call to + // TextView#setTransformationMethodInternal in enterInsertMode() or exitInsertMode(). + // Do nothing in this case. + if (mUpdatingTransformationMethod) { + return; + } + // TextView#setText is called externally. Exit InsertMode but don't update text again + // when calling setTransformationMethod. + exitInsertMode(/* updateText */ false); + } + + /** * Notify the {@link InsertModeController} before the TextView's * {@link TransformationMethod} is updated. If it's not in the insert mode, * the given method is directly returned. Otherwise, it will wrap the given transformation @@ -8205,6 +8245,9 @@ public class Editor { return mInsertModeController.enterInsertMode(offset); } + /** + * Exit insert mode if this editor is in insert mode. + */ void exitInsertMode() { if (mInsertModeController == null) return; mInsertModeController.exitInsertMode(); @@ -8217,7 +8260,7 @@ public class Editor { */ void setTransformationMethod(TransformationMethod method) { if (mInsertModeController == null || !mInsertModeController.mIsInsertModeActive) { - mTextView.setTransformationMethodInternal(method); + mTextView.setTransformationMethodInternal(method, /* updateText */ true); return; } @@ -8226,11 +8269,19 @@ public class Editor { final int selectionStart = mTextView.getSelectionStart(); final int selectionEnd = mTextView.getSelectionEnd(); method = mInsertModeController.updateTransformationMethod(method); - mTextView.setTransformationMethodInternal(method); + mTextView.setTransformationMethodInternal(method, /* updateText */ true); Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); } /** + * Notify that the Editor that the associated {@link TextView} is about to set its text. + */ + void beforeSetText() { + if (mInsertModeController == null) return; + mInsertModeController.beforeSetText(); + } + + /** * Initializes the nodeInfo with smart actions. */ void onInitializeSmartActionsAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) { diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 7e1e52dd0707..438b9742f0af 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -2795,11 +2795,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mEditor != null) { mEditor.setTransformationMethod(method); } else { - setTransformationMethodInternal(method); + setTransformationMethodInternal(method, /* updateText */ true); } } - void setTransformationMethodInternal(@Nullable TransformationMethod method) { + /** + * Set the transformation that is applied to the text that this TextView is displaying, + * optionally call the setText. + * @param method the new transformation method to be set. + * @param updateText whether the call {@link #setText} which will update the TextView to display + * the new content. This method is helpful when updating + * {@link TransformationMethod} inside {@link #setText}. It should only be + * false if text will be updated immediately after this call, otherwise the + * TextView will enter an inconsistent state. + */ + void setTransformationMethodInternal(@Nullable TransformationMethod method, + boolean updateText) { if (method == mTransformation) { // Avoid the setText() below if the transformation is // the same. @@ -2821,7 +2832,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mAllowTransformationLengthChange = false; } - setText(mText); + if (updateText) { + setText(mText); + } if (hasPasswordTransformationMethod()) { notifyViewAccessibilityStateChangedIfNeeded( @@ -7000,6 +7013,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @UnsupportedAppUsage private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) { + if (mEditor != null) { + mEditor.beforeSetText(); + } mTextSetFromXmlOrResourceId = false; if (text == null) { text = ""; @@ -13811,13 +13827,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Helper method to set {@code rect} to the text content's non-clipped area in the view's - * coordinates. + * Helper method to set {@code rect} to this TextView's non-clipped area in its own coordinates. + * This method obtains the view's visible rectangle whereas the method + * {@link #getContentVisibleRect} returns the text layout's visible rectangle. * * @return true if at least part of the text content is visible; false if the text content is * completely clipped or translated out of the visible area. */ - private boolean getContentVisibleRect(Rect rect) { + private boolean getViewVisibleRect(Rect rect) { if (!getLocalVisibleRect(rect)) { return false; } @@ -13826,6 +13843,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // view's coordinates. So we need to offset it with the negative scrolled amount to convert // it to view's coordinate. rect.offset(-getScrollX(), -getScrollY()); + return true; + } + + /** + * Helper method to set {@code rect} to the text content's non-clipped area in the view's + * coordinates. + * + * @return true if at least part of the text content is visible; false if the text content is + * completely clipped or translated out of the visible area. + */ + private boolean getContentVisibleRect(Rect rect) { + if (!getViewVisibleRect(rect)) { + return false; + } // Clip the view's visible rect with the text layout's visible rect. return rect.intersect(getCompoundPaddingLeft(), getCompoundPaddingTop(), getWidth() - getCompoundPaddingRight(), getHeight() - getCompoundPaddingBottom()); @@ -13955,14 +13986,25 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener builder.setMatrix(viewToScreenMatrix); if (includeEditorBounds) { - final RectF editorBounds = new RectF(); - editorBounds.set(0 /* left */, 0 /* top */, - getWidth(), getHeight()); - final RectF handwritingBounds = new RectF( - -getHandwritingBoundsOffsetLeft(), - -getHandwritingBoundsOffsetTop(), - getWidth() + getHandwritingBoundsOffsetRight(), - getHeight() + getHandwritingBoundsOffsetBottom()); + if (mTempRect == null) { + mTempRect = new Rect(); + } + final Rect bounds = mTempRect; + final RectF editorBounds; + final RectF handwritingBounds; + if (getViewVisibleRect(bounds)) { + editorBounds = new RectF(bounds); + handwritingBounds = new RectF(editorBounds); + handwritingBounds.top -= getHandwritingBoundsOffsetTop(); + handwritingBounds.left -= getHandwritingBoundsOffsetLeft(); + handwritingBounds.bottom += getHandwritingBoundsOffsetBottom(); + handwritingBounds.right += getHandwritingBoundsOffsetRight(); + } else { + // The editor is not visible at all, return empty rectangles. We still need to + // return an EditorBoundsInfo because IME has subscribed the EditorBoundsInfo. + editorBounds = new RectF(); + handwritingBounds = new RectF(); + } EditorBoundsInfo.Builder boundsBuilder = new EditorBoundsInfo.Builder(); EditorBoundsInfo editorBoundsInfo = boundsBuilder.setEditorBounds(editorBounds) .setHandwritingBounds(handwritingBounds).build(); diff --git a/core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java b/core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java index 1a019878f67f..62adc2001548 100644 --- a/core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java +++ b/core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java @@ -30,6 +30,7 @@ import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.EditorBoundsInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; @@ -54,8 +55,15 @@ public class EditTextCursorAnchorInfoTest { private static final int[] sLocationOnScreen = new int[2]; private static Typeface sTypeface; private static final float TEXT_SIZE = 1f; - // The line height of the test font font is 1.2 * textSize. + // The line height of the test font is 1.2 * textSize. private static final int LINE_HEIGHT = 12; + private static final int HW_BOUNDS_OFFSET_LEFT = 10; + private static final int HW_BOUNDS_OFFSET_TOP = 20; + private static final int HW_BOUNDS_OFFSET_RIGHT = 30; + private static final int HW_BOUNDS_OFFSET_BOTTOM = 40; + + + // Default text has 5 lines of text. The needed width is 50px and the needed height is 60px. private static final CharSequence DEFAULT_TEXT = "X\nXX\nXXX\nXXXX\nXXXXX"; private static final ImmutableList<RectF> DEFAULT_LINE_BOUNDS = ImmutableList.of( new RectF(0f, 0f, 10f, LINE_HEIGHT), @@ -131,6 +139,55 @@ public class EditTextCursorAnchorInfoTest { } @Test + public void testEditorBoundsInfo_allVisible() { + // The needed width and height of the DEFAULT_TEXT are 50 px and 60 px respectfully. + int width = 100; + int height = 200; + setupEditText(DEFAULT_TEXT, width, height); + CursorAnchorInfo cursorAnchorInfo = + mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix); + EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo(); + assertThat(editorBoundsInfo).isNotNull(); + assertThat(editorBoundsInfo.getEditorBounds()).isEqualTo(new RectF(0, 0, width, height)); + assertThat(editorBoundsInfo.getHandwritingBounds()) + .isEqualTo(new RectF(-HW_BOUNDS_OFFSET_LEFT, -HW_BOUNDS_OFFSET_TOP, + width + HW_BOUNDS_OFFSET_RIGHT, height + HW_BOUNDS_OFFSET_BOTTOM)); + } + + @Test + public void testEditorBoundsInfo_scrolled() { + // The height of the editor will be 60 px. + int width = 100; + int visibleTop = 10; + int visibleBottom = 30; + setupVerticalClippedEditText(width, visibleTop, visibleBottom); + CursorAnchorInfo cursorAnchorInfo = + mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix); + EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo(); + assertThat(editorBoundsInfo).isNotNull(); + assertThat(editorBoundsInfo.getEditorBounds()) + .isEqualTo(new RectF(0, visibleTop, width, visibleBottom)); + assertThat(editorBoundsInfo.getHandwritingBounds()) + .isEqualTo(new RectF(-HW_BOUNDS_OFFSET_LEFT, visibleTop - HW_BOUNDS_OFFSET_TOP, + width + HW_BOUNDS_OFFSET_RIGHT, visibleBottom + HW_BOUNDS_OFFSET_BOTTOM)); + } + + @Test + public void testEditorBoundsInfo_invisible() { + // The height of the editor will be 60px. Scroll it to 70px will make it invisible. + int width = 100; + int visibleTop = 70; + int visibleBottom = 70; + setupVerticalClippedEditText(width, visibleTop, visibleBottom); + CursorAnchorInfo cursorAnchorInfo = + mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix); + EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo(); + assertThat(editorBoundsInfo).isNotNull(); + assertThat(editorBoundsInfo.getEditorBounds()).isEqualTo(new RectF(0, 0, 0, 0)); + assertThat(editorBoundsInfo.getHandwritingBounds()).isEqualTo(new RectF(0, 0, 0, 0)); + } + + @Test public void testVisibleLineBounds_allVisible() { setupEditText(DEFAULT_TEXT, /* height= */ 100); CursorAnchorInfo cursorAnchorInfo = @@ -465,32 +522,26 @@ public class EditTextCursorAnchorInfoTest { } private void setupVerticalClippedEditText(int visibleTop, int visibleBottom) { - ScrollView scrollView = new ScrollView(mActivity); - mEditText = new EditText(mActivity); - mEditText.setTypeface(sTypeface); - mEditText.setText(DEFAULT_TEXT); - mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, TEXT_SIZE); - - mEditText.setPadding(0, 0, 0, 0); - mEditText.setCompoundDrawables(null, null, null, null); - mEditText.setCompoundDrawablePadding(0); - - mEditText.scrollTo(0, 0); - mEditText.setLineSpacing(0f, 1f); + setupVerticalClippedEditText(1000, visibleTop, visibleBottom); + } - // Place the text layout top to the view's top. - mEditText.setGravity(Gravity.TOP); - int width = 1000; - int height = visibleBottom - visibleTop; + /** + * Helper method to create an EditText in a vertical ScrollView so that its visible bounds + * is Rect(0, visibleTop, width, visibleBottom) in the EditText's coordinates. Both ScrollView + * and EditText's width is set to the given width. + */ + private void setupVerticalClippedEditText(int width, int visibleTop, int visibleBottom) { + ScrollView scrollView = new ScrollView(mActivity); + createEditText(); + int scrollViewHeight = visibleBottom - visibleTop; scrollView.addView(mEditText, new FrameLayout.LayoutParams( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(5 * LINE_HEIGHT, View.MeasureSpec.EXACTLY))); scrollView.measure( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); - scrollView.layout(0, 0, width, height); - + View.MeasureSpec.makeMeasureSpec(scrollViewHeight, View.MeasureSpec.EXACTLY)); + scrollView.layout(0, 0, width, scrollViewHeight); scrollView.scrollTo(0, visibleTop); } @@ -499,6 +550,11 @@ public class EditTextCursorAnchorInfoTest { measureEditText(height); } + private void setupEditText(CharSequence text, int width, int height) { + createEditText(text); + measureEditText(width, height); + } + private void setupEditText(CharSequence text, int height, float lineSpacing, float lineMultiplier) { createEditText(text); @@ -537,6 +593,8 @@ public class EditTextCursorAnchorInfoTest { mEditText.setTypeface(sTypeface); mEditText.setText(text); mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, TEXT_SIZE); + mEditText.setHandwritingBoundsOffsets(HW_BOUNDS_OFFSET_LEFT, HW_BOUNDS_OFFSET_TOP, + HW_BOUNDS_OFFSET_RIGHT, HW_BOUNDS_OFFSET_BOTTOM); mEditText.setPadding(0, 0, 0, 0); mEditText.setCompoundDrawables(null, null, null, null); |