Clamp EditText cursor in the drawable boundaries.
EditText tried to draw outside of the padding boundaries because of a
cursor positioning issue in RTL. This CL removes that fix and instead
clamps the cursor position if cursor is outside of the clipped view
boundary.
Bug: 23397961
Change-Id: Id5f1fbe2a0f571100c89b21758fbb81b14d5da57
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 3a61fcd..1826dd8 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -1851,8 +1851,7 @@
updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped));
if (mCursorCount == 2) {
- updateCursorPosition(1, middle, bottom,
- layout.getSecondaryHorizontal(offset, clamped));
+ updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped));
}
}
@@ -2151,21 +2150,60 @@
return mSelectionModifierCursorController;
}
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public Drawable[] getCursorDrawable() {
+ return mCursorDrawable;
+ }
+
private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
if (mCursorDrawable[cursorIndex] == null)
mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
mTextView.mCursorDrawableRes);
-
- if (mTempRect == null) mTempRect = new Rect();
- mCursorDrawable[cursorIndex].getPadding(mTempRect);
- final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
- horizontal = Math.max(0.5f, horizontal - 0.5f);
- final int left = (int) (horizontal) - mTempRect.left;
- mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
+ final Drawable drawable = mCursorDrawable[cursorIndex];
+ final int left = clampCursorHorizontalPosition(drawable, horizontal);
+ final int width = drawable.getIntrinsicWidth();
+ drawable.setBounds(left, top - mTempRect.top, left + width,
bottom + mTempRect.bottom);
}
/**
+ * Return clamped position for the cursor. If the cursor is within the boundaries of the view,
+ * then it is offset with the left padding of the cursor drawable. If the cursor is at
+ * the beginning or the end of the text then its drawable edge is aligned with left or right of
+ * the view boundary.
+ *
+ * @param drawable Cursor drawable.
+ * @param horizontal Horizontal position for the cursor.
+ * @return The clamped horizontal position for the cursor.
+ */
+ private final int clampCursorHorizontalPosition(final Drawable drawable, float
+ horizontal) {
+ horizontal = Math.max(0.5f, horizontal - 0.5f);
+ if (mTempRect == null) mTempRect = new Rect();
+ drawable.getPadding(mTempRect);
+ int scrollX = mTextView.getScrollX();
+ float horizontalDiff = horizontal - scrollX;
+ int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
+ - mTextView.getCompoundPaddingRight();
+
+ final int left;
+ if (horizontalDiff >= (viewClippedWidth - 1f)) {
+ // at the rightmost position
+ final int cursorWidth = drawable.getIntrinsicWidth();
+ left = viewClippedWidth + scrollX - (cursorWidth - mTempRect.right);
+ } else if (Math.abs(horizontalDiff) <= 1f) {
+ // at the leftmost position
+ left = scrollX - mTempRect.left;
+ } else {
+ left = (int) horizontal - mTempRect.left;
+ }
+ return left;
+ }
+
+ /**
* Called by the framework in response to a text auto-correction (such as fixing a typo using a
* a dictionary) from the current input method, provided by it calling
* {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
@@ -3919,8 +3957,8 @@
final Layout layout = mTextView.getLayout();
if (layout != null && oldDrawable != mDrawable && isShowing()) {
// Update popup window position.
- mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
- getHorizontalOffset() + getCursorOffset());
+ mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
+ getHorizontalOffset() + getCursorOffset();
mPositionX += mTextView.viewportToContentHorizontalOffset();
mPositionHasChanged = true;
updatePosition(mLastParentX, mLastParentY, false, false);
@@ -4049,8 +4087,8 @@
final int line = layout.getLineForOffset(offset);
mPrevLine = line;
- mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
- getHorizontalOffset() + getCursorOffset());
+ mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
+ getHorizontalOffset() + getCursorOffset();
mPositionY = layout.getLineBottom(line);
// Take TextView's padding and scroll into account.
@@ -4062,6 +4100,17 @@
}
}
+ /**
+ * Return the clamped horizontal position for the first cursor.
+ *
+ * @param layout Text layout.
+ * @param offset Character offset for the cursor.
+ * @return The clamped horizontal position for the cursor.
+ */
+ int getCursorHorizontalPosition(Layout layout, int offset) {
+ return (int) (layout.getPrimaryHorizontal(offset) - 0.5f);
+ }
+
public void updatePosition(int parentPositionX, int parentPositionY,
boolean parentPositionChanged, boolean parentScrolled) {
positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
@@ -4300,6 +4349,16 @@
}
@Override
+ int getCursorHorizontalPosition(Layout layout, int offset) {
+ final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
+ if (drawable != null) {
+ final float horizontal = layout.getPrimaryHorizontal(offset);
+ return clampCursorHorizontalPosition(drawable, horizontal) + mTempRect.left;
+ }
+ return super.getCursorHorizontalPosition(layout, offset);
+ }
+
+ @Override
public boolean onTouchEvent(MotionEvent ev) {
final boolean result = super.onTouchEvent(ev);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 712a04b..ec9cb63 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5452,15 +5452,9 @@
return (int) Math.max(0, mShadowDy + mShadowRadius);
}
- private int getFudgedPaddingRight() {
- // Add sufficient space for cursor and tone marks
- int cursorWidth = 2 + (int)mTextPaint.density; // adequate for Material cursors
- return Math.max(0, getCompoundPaddingRight() - (cursorWidth - 1));
- }
-
@Override
protected int getRightPaddingOffset() {
- return -(getFudgedPaddingRight() - mPaddingRight) +
+ return -(getCompoundPaddingRight() - mPaddingRight) +
(int) Math.max(0, mShadowDx + mShadowRadius);
}
@@ -5805,7 +5799,7 @@
float clipLeft = compoundPaddingLeft + scrollX;
float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
- float clipRight = right - left - getFudgedPaddingRight() + scrollX;
+ float clipRight = right - left - getCompoundPaddingRight() + scrollX;
float clipBottom = bottom - top + scrollY -
((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
diff --git a/core/tests/coretests/src/android/widget/EditorCursorTest.java b/core/tests/coretests/src/android/widget/EditorCursorTest.java
new file mode 100644
index 0000000..04c8b8c
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/EditorCursorTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.ViewGroup;
+
+public class EditorCursorTest extends ActivityInstrumentationTestCase2<TextViewActivity> {
+
+ private EditText mEditText;
+ private final String RTL_STRING = "مرحبا الروبوت مرحبا الروبوت مرحبا الروبوت";
+
+ public EditorCursorTest() {
+ super(TextViewActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mEditText = new EditText(getActivity());
+ mEditText.setTextSize(30);
+ mEditText.setSingleLine(true);
+ mEditText.setLines(1);
+ mEditText.setPadding(15, 15, 15, 15);
+ ViewGroup.LayoutParams editTextLayoutParams = new ViewGroup.LayoutParams(200,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mEditText.setLayoutParams(editTextLayoutParams);
+
+ final FrameLayout layout = new FrameLayout(getActivity());
+ ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ layout.setLayoutParams(layoutParams);
+ layout.addView(mEditText);
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ getActivity().setContentView(layout);
+ mEditText.requestFocus();
+ }
+ });
+ getInstrumentation().waitForIdleSync();
+ }
+
+ @SmallTest
+ public void testCursorIsInViewBoundariesWhenOnRightForLtr() throws Exception {
+ // Asserts that when an EditText has LTR text, and cursor is at the end (right),
+ // cursor is drawn to the right edge of the view
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mEditText.setText("aaaaaaaaaaaaaaaaaaaaaa");
+ int length = mEditText.getText().length();
+ mEditText.setSelection(length, length);
+ }
+ });
+ getInstrumentation().waitForIdleSync();
+
+ Editor editor = mEditText.getEditorForTesting();
+ Drawable drawable = editor.getCursorDrawable()[0];
+ Rect drawableBounds = drawable.getBounds();
+ Rect drawablePadding = new Rect();
+ drawable.getPadding(drawablePadding);
+
+ // right edge of the view including the scroll
+ int maxRight = mEditText.getWidth() - mEditText.getCompoundPaddingRight()
+ - mEditText.getCompoundPaddingLeft() + +mEditText.getScrollX();
+ int diff = drawableBounds.right - drawablePadding.right - maxRight;
+ assertTrue(diff >= 0 && diff <= 1);
+ }
+
+ @SmallTest
+ public void testCursorIsInViewBoundariesWhenOnLeftForLtr() throws Exception {
+ // Asserts that when an EditText has LTR text, and cursor is at the beginning,
+ // cursor is drawn to the left edge of the view
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mEditText.setText("aaaaaaaaaaaaaaaaaaaaaa");
+ mEditText.setSelection(0, 0);
+ }
+ });
+ getInstrumentation().waitForIdleSync();
+
+ Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
+ Rect drawableBounds = drawable.getBounds();
+ Rect drawablePadding = new Rect();
+ drawable.getPadding(drawablePadding);
+
+ int diff = drawableBounds.left + drawablePadding.left;
+ assertTrue(diff >= 0 && diff <= 1);
+ }
+
+ @SmallTest
+ public void testCursorIsInViewBoundariesWhenOnRightForRtl() throws Exception {
+ // Asserts that when an EditText has RTL text, and cursor is at the end,
+ // cursor is drawn to the left edge of the view
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mEditText.setText(RTL_STRING);
+ mEditText.setSelection(0, 0);
+ }
+ });
+ getInstrumentation().waitForIdleSync();
+
+ Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
+ Rect drawableBounds = drawable.getBounds();
+ Rect drawablePadding = new Rect();
+ drawable.getPadding(drawablePadding);
+
+ int maxRight = mEditText.getWidth() - mEditText.getCompoundPaddingRight()
+ - mEditText.getCompoundPaddingLeft() + mEditText.getScrollX();
+
+ int diff = drawableBounds.right - drawablePadding.right - maxRight;
+ assertTrue(diff >= 0 && diff <= 1);
+ }
+
+ @SmallTest
+ public void testCursorIsInViewBoundariesWhenOnLeftForRtl() throws Exception {
+ // Asserts that when an EditText has RTL text, and cursor is at the beginning,
+ // cursor is drawn to the right edge of the view
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mEditText.setText(RTL_STRING);
+ int length = mEditText.getText().length();
+ mEditText.setSelection(length, length);
+ }
+ });
+ getInstrumentation().waitForIdleSync();
+
+ Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
+ Rect drawableBounds = drawable.getBounds();
+ Rect drawablePadding = new Rect();
+ drawable.getPadding(drawablePadding);
+
+ int diff = drawableBounds.left - mEditText.getScrollX() + drawablePadding.left;
+ assertTrue(diff >= 0 && diff <= 1);
+ }
+
+}