From daf43494d0532903ddf5d769c5381f4c126ec418 Mon Sep 17 00:00:00 2001 From: Haoyu Zhang Date: Tue, 20 Jun 2023 13:47:06 -0700 Subject: [PATCH] Fix: CursorAnchorInfo EditorBoundsInfo includes non-visible region Bug: 277625776 Test: atest EditTextCursorAnchorInfoTest Change-Id: I1c45951d32b0ef394ba1857846c056d7f5f6da97 --- core/java/android/widget/TextView.java | 48 ++++++--- .../widget/EditTextCursorAnchorInfoTest.java | 98 +++++++++++++++---- 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 7e1e52dd07079..269cef19fc732 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -13811,13 +13811,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 +13827,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 +13970,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 1a019878f67ff..62adc2001548d 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 DEFAULT_LINE_BOUNDS = ImmutableList.of( new RectF(0f, 0f, 10f, LINE_HEIGHT), @@ -130,6 +138,55 @@ public class EditTextCursorAnchorInfoTest { assertThat(actualMatrix).isEqualTo(expectedMatrix); } + @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); @@ -465,32 +522,26 @@ public class EditTextCursorAnchorInfoTest { } private void setupVerticalClippedEditText(int visibleTop, int visibleBottom) { + setupVerticalClippedEditText(1000, visibleTop, visibleBottom); + } + + /** + * 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); - 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); - - // Place the text layout top to the view's top. - mEditText.setGravity(Gravity.TOP); - int width = 1000; - int height = visibleBottom - visibleTop; + 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);