return;
}
- Layout layout = getActiveLayout();
+ Layout layout = mTextView.getLayout();
final int offset = mTextView.getSelectionStart();
final int line = layout.getLineForOffset(offset);
final int top = layout.getLineTop(line);
mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
mTextView.mCursorDrawableRes);
final Drawable drawable = mCursorDrawable[cursorIndex];
- final int left = clampCursorHorizontalPosition(drawable, horizontal);
+ final int left = clampHorizontalPosition(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
+ * Return clamped position for the drawable. If the drawable is within the boundaries of the
+ * view, then it is offset with the left padding of the cursor drawable. If the drawable is at
* the beginning or the end of the text then its drawable edge is aligned with left or right of
- * the view boundary.
+ * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
+ * of the view.
*
- * @param drawable Cursor drawable.
- * @param horizontal Horizontal position for the cursor.
- * @return The clamped horizontal position for the cursor.
+ * @param drawable Drawable. Can be null.
+ * @param horizontal Horizontal position for the drawable.
+ * @return The clamped horizontal position for the drawable.
*/
- private final int clampCursorHorizontalPosition(final Drawable drawable, float
- horizontal) {
+ private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
horizontal = Math.max(0.5f, horizontal - 0.5f);
if (mTempRect == null) mTempRect = new Rect();
- drawable.getPadding(mTempRect);
+
+ int drawableWidth = 0;
+ if (drawable != null) {
+ drawable.getPadding(mTempRect);
+ drawableWidth = drawable.getIntrinsicWidth();
+ } else {
+ mTempRect.setEmpty();
+ }
+
int scrollX = mTextView.getScrollX();
float horizontalDiff = horizontal - scrollX;
int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
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) {
+ left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
+ } else if (Math.abs(horizontalDiff) <= 1f ||
+ (TextUtils.isEmpty(mTextView.getText())
+ && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
+ && horizontal <= 1f)) {
// at the leftmost position
left = scrollX - mTempRect.left;
} else {
+ mHandleHeight);
} else {
// We have a single cursor.
- Layout layout = getActiveLayout();
+ Layout layout = mTextView.getLayout();
int line = layout.getLineForOffset(mTextView.getSelectionStart());
- float primaryHorizontal =
- layout.getPrimaryHorizontal(mTextView.getSelectionStart());
+ float primaryHorizontal = clampHorizontalPosition(null,
+ layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
mSelectionBounds.set(
primaryHorizontal,
layout.getLineTop(line),
prepareCursorControllers();
return;
}
- layout = getActiveLayout();
+ layout = mTextView.getLayout();
boolean offsetChanged = offset != mPreviousOffset;
if (offsetChanged || parentScrolled) {
public void onDetached() {}
}
- /**
- * Returns the active layout (hint or text layout). Note that the text layout can be null.
- */
- private Layout getActiveLayout() {
- Layout layout = mTextView.getLayout();
- Layout hintLayout = mTextView.getHintLayout();
- if (TextUtils.isEmpty(layout.getText()) && hintLayout != null &&
- !TextUtils.isEmpty(hintLayout.getText())) {
- layout = hintLayout;
- }
- return layout;
- }
-
private class InsertionHandleView extends HandleView {
private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
if (drawable != null) {
final float horizontal = layout.getPrimaryHorizontal(offset);
- return clampCursorHorizontalPosition(drawable, horizontal) + mTempRect.left;
+ return clampHorizontalPosition(drawable, horizontal) + mTempRect.left;
}
return super.getCursorHorizontalPosition(layout, offset);
}
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.Choreographer;
import android.view.ViewGroup;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.widget.espresso.TextViewAssertions.hasInsertionPointerOnLeft;
+import static android.widget.espresso.TextViewAssertions.hasInsertionPointerOnRight;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.isEmptyString;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.sameInstance;
+
public class EditorCursorTest extends ActivityInstrumentationTestCase2<TextViewActivity> {
+
+ private final static String LTR_STRING = "aaaaaaaaaaaaaaaaaaaaaa";
+ private final static String LTR_HINT = "hint";
+ private final static String RTL_STRING = "مرحبا الروبوت مرحبا الروبوت مرحبا الروبوت";
+ private final static String RTL_HINT = "الروبوت";
+ private final static int CURSOR_BLINK_MS = 500;
+
private EditText mEditText;
- private final String RTL_STRING = "مرحبا الروبوت مرحبا الروبوت مرحبا الروبوت";
public EditorCursorTest() {
super(TextViewActivity.class);
@Override
public void run() {
getActivity().setContentView(layout);
- mEditText.requestFocus();
}
});
getInstrumentation().waitForIdleSync();
+ onView(sameInstance(mEditText)).perform(click());
}
@SmallTest
- public void testCursorIsInViewBoundariesWhenOnRightForLtr() throws Exception {
+ public void testCursorIsInViewBoundariesWhenOnRightForLtr() {
// 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();
+ setEditTextText(LTR_STRING, LTR_STRING.length());
- 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);
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
}
@SmallTest
- public void testCursorIsInViewBoundariesWhenOnLeftForLtr() throws Exception {
+ public void testCursorIsInViewBoundariesWhenOnLeftForLtr() {
// Asserts that when an EditText has LTR text, and cursor is at the beginning,
// cursor is drawn to the left edge of the view
+ setEditTextText(LTR_STRING, 0);
- 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);
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
}
@SmallTest
- public void testCursorIsInViewBoundariesWhenOnRightForRtl() throws Exception {
+ public void testCursorIsInViewBoundariesWhenOnRightForRtl() {
// Asserts that when an EditText has RTL text, and cursor is at the end,
// cursor is drawn to the left edge of the view
+ setEditTextText(RTL_STRING, 0);
- getActivity().runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mEditText.setText(RTL_STRING);
- mEditText.setSelection(0, 0);
- }
- });
- getInstrumentation().waitForIdleSync();
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+ }
+
+ @SmallTest
+ public void testCursorIsInViewBoundariesWhenOnLeftForRtl() {
+ // Asserts that when an EditText has RTL text, and cursor is at the beginning,
+ // cursor is drawn to the right edge of the view
+ setEditTextText(RTL_STRING, RTL_STRING.length());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
+ }
+
+ /* Tests for cursor positioning with hint */
+ @SmallTest
+ public void testCursorIsOnLeft_withFirstStrongLtrAlgorithm() {
+ setEditTextHint(null, TextView.TEXT_DIRECTION_FIRST_STRONG_LTR, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+ assertThat(mEditText.getHint(), nullValue());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
- Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
- Rect drawableBounds = drawable.getBounds();
- Rect drawablePadding = new Rect();
- drawable.getPadding(drawablePadding);
+ setEditTextHint(RTL_HINT, TextView.TEXT_DIRECTION_FIRST_STRONG_LTR, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
- int maxRight = mEditText.getWidth() - mEditText.getCompoundPaddingRight()
- - mEditText.getCompoundPaddingLeft() + mEditText.getScrollX();
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
- int diff = drawableBounds.right - drawablePadding.right - maxRight;
- assertTrue(diff >= 0 && diff <= 1);
+ setEditTextHint(LTR_HINT, TextView.TEXT_DIRECTION_FIRST_STRONG_LTR, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
}
@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
+ public void testCursorIsOnRight_withFirstStrongRtlAlgorithm() {
+ setEditTextHint(null, TextView.TEXT_DIRECTION_FIRST_STRONG_RTL, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+ assertThat(mEditText.getHint(), nullValue());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+
+ setEditTextHint(LTR_HINT, TextView.TEXT_DIRECTION_FIRST_STRONG_RTL, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+
+ setEditTextHint(RTL_HINT, TextView.TEXT_DIRECTION_FIRST_STRONG_RTL, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+ }
+
+ @SmallTest
+ public void testCursorIsOnLeft_withLtrAlgorithm() {
+ setEditTextHint(null, TextView.TEXT_DIRECTION_LTR, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+ assertThat(mEditText.getHint(), nullValue());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
+
+ setEditTextHint(RTL_HINT, TextView.TEXT_DIRECTION_LTR, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
+ setEditTextHint(LTR_HINT, TextView.TEXT_DIRECTION_LTR, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnLeft());
+ }
+
+ @SmallTest
+ public void testCursorIsOnRight_withRtlAlgorithm() {
+ setEditTextHint(null, TextView.TEXT_DIRECTION_RTL, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+ assertThat(mEditText.getHint(), nullValue());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+
+ setEditTextHint(LTR_HINT, TextView.TEXT_DIRECTION_RTL, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+
+ setEditTextHint(RTL_HINT, TextView.TEXT_DIRECTION_RTL, 0);
+ assertThat(mEditText.getText().toString(), isEmptyString());
+
+ onView(sameInstance(mEditText)).check(hasInsertionPointerOnRight());
+ }
+
+ private void setEditTextProperties(final String text, final String hint,
+ final Integer textDirection, final Integer selection) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
- mEditText.setText(RTL_STRING);
- int length = mEditText.getText().length();
- mEditText.setSelection(length, length);
+ if (textDirection != null) mEditText.setTextDirection(textDirection);
+ if (text != null) mEditText.setText(text);
+ if (hint != null) mEditText.setHint(hint);
+ if (selection != null) mEditText.setSelection(selection);
}
});
getInstrumentation().waitForIdleSync();
- Drawable drawable = mEditText.getEditorForTesting().getCursorDrawable()[0];
- Rect drawableBounds = drawable.getBounds();
- Rect drawablePadding = new Rect();
- drawable.getPadding(drawablePadding);
+ // wait for cursor to be drawn. updateCursorPositions function is called during draw() and
+ // only when cursor is visible during blink.
+ final CountDownLatch latch = new CountDownLatch(1);
+ mEditText.postOnAnimationDelayed(new Runnable() {
+ @Override
+ public void run() {
+ latch.countDown();
+ }
+ }, CURSOR_BLINK_MS);
+ try {
+ assertThat("Problem while waiting for the cursor to blink",
+ latch.await(10, TimeUnit.SECONDS), equalTo(true));
+ } catch (Exception e) {
+ fail("Problem while waiting for the cursor to blink");
+ }
+ }
- int diff = drawableBounds.left - mEditText.getScrollX() + drawablePadding.left;
- assertTrue(diff >= 0 && diff <= 1);
+ private void setEditTextHint(final String hint, final int textDirection, final int selection) {
+ setEditTextProperties(null, hint, textDirection, selection);
}
+ private void setEditTextText(final String text, final Integer selection) {
+ setEditTextProperties(text, null, null, selection);
+ }
}
import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
import static com.android.internal.util.Preconditions.checkNotNull;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.number.IsCloseTo.closeTo;
+import android.annotation.IntDef;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.ViewAssertion;
import android.view.View;
+import android.widget.EditText;
import android.widget.TextView;
import junit.framework.AssertionFailedError;
import org.hamcrest.Matcher;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* A collection of assertions on a {@link android.widget.TextView}.
*/
}
/**
+ * Returns a {@link ViewAssertion} that asserts that the EditText insertion pointer is on
+ * the left edge.
+ */
+ public static ViewAssertion hasInsertionPointerOnLeft() {
+ return new CursorPositionAssertion(CursorPositionAssertion.LEFT);
+ }
+
+ /**
+ * Returns a {@link ViewAssertion} that asserts that the EditText insertion pointer is on
+ * the right edge.
+ */
+ public static ViewAssertion hasInsertionPointerOnRight() {
+ return new CursorPositionAssertion(CursorPositionAssertion.RIGHT);
+ }
+
+ /**
* A {@link ViewAssertion} to check the selected text in a {@link TextView}.
*/
private static final class TextSelectionAssertion implements ViewAssertion {
}
}
}
+
+ /**
+ * {@link ViewAssertion} to check that EditText cursor is on a given position.
+ */
+ static class CursorPositionAssertion implements ViewAssertion {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({LEFT, RIGHT})
+ public @interface CursorEdgePositionType {}
+ public static final int LEFT = 0;
+ public static final int RIGHT = 1;
+
+ private final int mPosition;
+
+ private CursorPositionAssertion(@CursorEdgePositionType int position) {
+ this.mPosition = position;
+ }
+
+ @Override
+ public void check(View view, NoMatchingViewException exception) {
+ if (!(view instanceof EditText)) {
+ throw new AssertionFailedError("View should be an instance of EditText");
+ }
+ EditText editText = (EditText) view;
+ Drawable drawable = editText.getEditorForTesting().getCursorDrawable()[0];
+ Rect drawableBounds = drawable.getBounds();
+ Rect drawablePadding = new Rect();
+ drawable.getPadding(drawablePadding);
+
+ final int diff;
+ final String positionStr;
+ switch (mPosition) {
+ case LEFT:
+ positionStr = "left";
+ diff = drawableBounds.left - editText.getScrollX() + drawablePadding.left;
+ break;
+ case RIGHT:
+ positionStr = "right";
+ int maxRight = editText.getWidth() - editText.getCompoundPaddingRight()
+ - editText.getCompoundPaddingLeft() + editText.getScrollX();
+ diff = drawableBounds.right - drawablePadding.right - maxRight;
+ break;
+ default:
+ throw new AssertionFailedError("Unknown position for cursor assertion");
+ }
+
+ assertThat("Cursor should be on the " + positionStr, Double.valueOf(diff),
+ closeTo(0f, 1f));
+ }
+ }
}