Loading core/java/android/text/flags/flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -119,3 +119,10 @@ flag { is_fixed_read_only: true bug: "324676775" } flag { name: "handwriting_cursor_position" namespace: "text" description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph." bug: "323376217" } core/java/android/view/HandwritingInitiator.java +15 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package android.view; import static com.android.text.flags.Flags.handwritingCursorPosition; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; Loading Loading @@ -557,7 +559,8 @@ public class HandwritingInitiator { } private void requestFocusWithoutReveal(View view) { if (view instanceof EditText editText && !mState.mStylusDownWithinEditorBounds) { if (!handwritingCursorPosition() && view instanceof EditText editText && !mState.mStylusDownWithinEditorBounds) { // If the stylus down point was inside the EditText's bounds, then the EditText will // automatically set its cursor position nearest to the stylus down point when it // gains focus. If the stylus down point was outside the EditText's bounds (within Loading @@ -576,6 +579,17 @@ public class HandwritingInitiator { } else { view.requestFocus(); } if (handwritingCursorPosition() && view instanceof EditText editText) { // Move the cursor to the end of the paragraph closest to the stylus down point. view.getLocationInWindow(mTempLocation); int line = editText.getLineAtCoordinate(mState.mStylusDownY - mTempLocation[1]); int paragraphEnd = TextUtils.indexOf(editText.getText(), '\n', editText.getLayout().getLineStart(line)); if (paragraphEnd < 0) { paragraphEnd = editText.getText().length(); } editText.setSelection(paragraphEnd); } } /** Loading core/java/android/widget/TextView.java +2 −1 Original line number Diff line number Diff line Loading @@ -15490,8 +15490,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return x; } /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) int getLineAtCoordinate(float y) { public int getLineAtCoordinate(float y) { y -= getTotalPaddingTop(); // Clamp the position to inside of the view. y = Math.max(0.0f, y); core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +67 −9 Original line number Diff line number Diff line Loading @@ -23,6 +23,8 @@ import static android.view.MotionEvent.ACTION_UP; import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import static android.view.stylus.HandwritingTestUtil.createView; import static com.android.text.flags.Flags.handwritingCursorPosition; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeFalse; Loading Loading @@ -129,6 +131,44 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_when_stylusMoveOnce_withinHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); mHandwritingInitiator.onInputConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); boolean onTouchEventResult1 = mHandwritingInitiator.onTouchEvent(stylusEvent1); final int x2 = x1 + mHandwritingSlop * 2; final int y2 = y1; MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); boolean onTouchEventResult2 = mHandwritingInitiator.onTouchEvent(stylusEvent2); // Stylus movement within HandwritingArea should trigger IMM.startHandwriting once. verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); assertThat(onTouchEventResult1).isFalse(); // After IMM.startHandwriting is triggered, onTouchEvent should return true for ACTION_MOVE // events so that the events are not dispatched to the view tree. assertThat(onTouchEventResult2).isTrue(); if (handwritingCursorPosition()) { // Cursor is placed at the end of the text. verify(mTestView1).setSelection(5); } else { // Since the stylus down point was inside the TextView's bounds, the handwriting // initiator does not need to set the cursor position. verify(mTestView1, never()).setSelection(anyInt()); } } @Test public void onTouchEvent_startHandwriting_multipleParagraphs() { // End of line 0 is offset 10, end of line 1 is offset 20, end of line 2 is offset 30, end // of line 3 is offset 40. mTestView1.setText("line 0 \nline 1 \nline 2 \nline 3 "); mTestView1.layout(0, 0, 500, 500); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(2); mHandwritingInitiator.onInputConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; Loading @@ -148,10 +188,15 @@ public class HandwritingInitiatorTest { // After IMM.startHandwriting is triggered, onTouchEvent should return true for ACTION_MOVE // events so that the events are not dispatched to the view tree. assertThat(onTouchEventResult2).isTrue(); // Since the stylus down point was inside the TextView's bounds, the handwriting initiator // does not need to set the cursor position. if (handwritingCursorPosition()) { // Cursor is placed at the end of the paragraph containing line 2. verify(mTestView1).setSelection(30); } else { // Since the stylus down point was inside the TextView's bounds, the handwriting // initiator does not need to set the cursor position. verify(mTestView1, never()).setSelection(anyInt()); } } @Test public void onTouchEvent_startHandwritingOnce_when_stylusMoveMultiTimes_withinHWArea() { Loading Loading @@ -197,6 +242,7 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_when_stylusMove_withinExtendedHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); if (!mInitiateWithoutConnection) { mHandwritingInitiator.onInputConnectionCreated(mTestView1); Loading @@ -214,10 +260,15 @@ public class HandwritingInitiatorTest { // Stylus movement within extended HandwritingArea should trigger IMM.startHandwriting once. verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); // Since the stylus down point was outside the TextView's bounds, the handwriting initiator // sets the cursor position. if (handwritingCursorPosition()) { // Cursor is placed at the end of the text. verify(mTestView1).setSelection(5); } else { // Since the stylus down point was outside the TextView's bounds, the handwriting // initiator sets the cursor position. verify(mTestView1).setSelection(4); } } @Test public void onTouchEvent_startHandwriting_servedViewUpdateAfterStylusMove() { Loading Loading @@ -246,6 +297,8 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_servedViewUpdate_stylusMoveInExtendedHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); // The stylus down point is between mTestView1 and mTestView2, but it is within the // extended handwriting area of both views. It is closer to mTestView1. final int x1 = sHwArea1.right + HW_BOUNDS_OFFSETS_RIGHT_PX / 2; Loading Loading @@ -278,10 +331,15 @@ public class HandwritingInitiatorTest { // Handwriting is started for this view since the stylus down point is closest to this // view. verify(mHandwritingInitiator).startHandwriting(mTestView1); // Since the stylus down point was outside the TextView's bounds, the handwriting initiator // sets the cursor position. if (handwritingCursorPosition()) { // Cursor is placed at the end of the text. verify(mTestView1).setSelection(5); } else { // Since the stylus down point was outside the TextView's bounds, the handwriting // initiator sets the cursor position. verify(mTestView1).setSelection(4); } } @Test Loading Loading
core/java/android/text/flags/flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -119,3 +119,10 @@ flag { is_fixed_read_only: true bug: "324676775" } flag { name: "handwriting_cursor_position" namespace: "text" description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph." bug: "323376217" }
core/java/android/view/HandwritingInitiator.java +15 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package android.view; import static com.android.text.flags.Flags.handwritingCursorPosition; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; Loading Loading @@ -557,7 +559,8 @@ public class HandwritingInitiator { } private void requestFocusWithoutReveal(View view) { if (view instanceof EditText editText && !mState.mStylusDownWithinEditorBounds) { if (!handwritingCursorPosition() && view instanceof EditText editText && !mState.mStylusDownWithinEditorBounds) { // If the stylus down point was inside the EditText's bounds, then the EditText will // automatically set its cursor position nearest to the stylus down point when it // gains focus. If the stylus down point was outside the EditText's bounds (within Loading @@ -576,6 +579,17 @@ public class HandwritingInitiator { } else { view.requestFocus(); } if (handwritingCursorPosition() && view instanceof EditText editText) { // Move the cursor to the end of the paragraph closest to the stylus down point. view.getLocationInWindow(mTempLocation); int line = editText.getLineAtCoordinate(mState.mStylusDownY - mTempLocation[1]); int paragraphEnd = TextUtils.indexOf(editText.getText(), '\n', editText.getLayout().getLineStart(line)); if (paragraphEnd < 0) { paragraphEnd = editText.getText().length(); } editText.setSelection(paragraphEnd); } } /** Loading
core/java/android/widget/TextView.java +2 −1 Original line number Diff line number Diff line Loading @@ -15490,8 +15490,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return x; } /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) int getLineAtCoordinate(float y) { public int getLineAtCoordinate(float y) { y -= getTotalPaddingTop(); // Clamp the position to inside of the view. y = Math.max(0.0f, y);
core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +67 −9 Original line number Diff line number Diff line Loading @@ -23,6 +23,8 @@ import static android.view.MotionEvent.ACTION_UP; import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import static android.view.stylus.HandwritingTestUtil.createView; import static com.android.text.flags.Flags.handwritingCursorPosition; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeFalse; Loading Loading @@ -129,6 +131,44 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_when_stylusMoveOnce_withinHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); mHandwritingInitiator.onInputConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); boolean onTouchEventResult1 = mHandwritingInitiator.onTouchEvent(stylusEvent1); final int x2 = x1 + mHandwritingSlop * 2; final int y2 = y1; MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); boolean onTouchEventResult2 = mHandwritingInitiator.onTouchEvent(stylusEvent2); // Stylus movement within HandwritingArea should trigger IMM.startHandwriting once. verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); assertThat(onTouchEventResult1).isFalse(); // After IMM.startHandwriting is triggered, onTouchEvent should return true for ACTION_MOVE // events so that the events are not dispatched to the view tree. assertThat(onTouchEventResult2).isTrue(); if (handwritingCursorPosition()) { // Cursor is placed at the end of the text. verify(mTestView1).setSelection(5); } else { // Since the stylus down point was inside the TextView's bounds, the handwriting // initiator does not need to set the cursor position. verify(mTestView1, never()).setSelection(anyInt()); } } @Test public void onTouchEvent_startHandwriting_multipleParagraphs() { // End of line 0 is offset 10, end of line 1 is offset 20, end of line 2 is offset 30, end // of line 3 is offset 40. mTestView1.setText("line 0 \nline 1 \nline 2 \nline 3 "); mTestView1.layout(0, 0, 500, 500); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(2); mHandwritingInitiator.onInputConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; Loading @@ -148,10 +188,15 @@ public class HandwritingInitiatorTest { // After IMM.startHandwriting is triggered, onTouchEvent should return true for ACTION_MOVE // events so that the events are not dispatched to the view tree. assertThat(onTouchEventResult2).isTrue(); // Since the stylus down point was inside the TextView's bounds, the handwriting initiator // does not need to set the cursor position. if (handwritingCursorPosition()) { // Cursor is placed at the end of the paragraph containing line 2. verify(mTestView1).setSelection(30); } else { // Since the stylus down point was inside the TextView's bounds, the handwriting // initiator does not need to set the cursor position. verify(mTestView1, never()).setSelection(anyInt()); } } @Test public void onTouchEvent_startHandwritingOnce_when_stylusMoveMultiTimes_withinHWArea() { Loading Loading @@ -197,6 +242,7 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_when_stylusMove_withinExtendedHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); if (!mInitiateWithoutConnection) { mHandwritingInitiator.onInputConnectionCreated(mTestView1); Loading @@ -214,10 +260,15 @@ public class HandwritingInitiatorTest { // Stylus movement within extended HandwritingArea should trigger IMM.startHandwriting once. verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); // Since the stylus down point was outside the TextView's bounds, the handwriting initiator // sets the cursor position. if (handwritingCursorPosition()) { // Cursor is placed at the end of the text. verify(mTestView1).setSelection(5); } else { // Since the stylus down point was outside the TextView's bounds, the handwriting // initiator sets the cursor position. verify(mTestView1).setSelection(4); } } @Test public void onTouchEvent_startHandwriting_servedViewUpdateAfterStylusMove() { Loading Loading @@ -246,6 +297,8 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_servedViewUpdate_stylusMoveInExtendedHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); // The stylus down point is between mTestView1 and mTestView2, but it is within the // extended handwriting area of both views. It is closer to mTestView1. final int x1 = sHwArea1.right + HW_BOUNDS_OFFSETS_RIGHT_PX / 2; Loading Loading @@ -278,10 +331,15 @@ public class HandwritingInitiatorTest { // Handwriting is started for this view since the stylus down point is closest to this // view. verify(mHandwritingInitiator).startHandwriting(mTestView1); // Since the stylus down point was outside the TextView's bounds, the handwriting initiator // sets the cursor position. if (handwritingCursorPosition()) { // Cursor is placed at the end of the text. verify(mTestView1).setSelection(5); } else { // Since the stylus down point was outside the TextView's bounds, the handwriting // initiator sets the cursor position. verify(mTestView1).setSelection(4); } } @Test Loading