summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/widget/Editor.java59
-rw-r--r--core/java/android/widget/TextView.java70
-rw-r--r--core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java98
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);