Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 88f4cc5d authored by Justin Ghan's avatar Justin Ghan
Browse files

Set cursor position in handwriting initiation

When handwriting is initiated for an unfocused EditText, 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 the extended handwriting bounds), then we must calculate
and set the cursor position manually.

Bug: 288461219
Test: atest HandwritingInitiatorTest
Change-Id: I62593f1ff01a7ec39c2eb6e1352d63a7f8b79f88
parent 1b2195f6
Loading
Loading
Loading
Loading
+32 −2
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;

import com.android.internal.annotations.VisibleForTesting;
@@ -81,6 +82,8 @@ public class HandwritingInitiator {
    private int mConnectionCount = 0;
    private final InputMethodManager mImm;

    private final int[] mTempLocation = new int[2];

    private final Rect mTempRect = new Rect();

    private final RectF mTempRectF = new RectF();
@@ -429,7 +432,19 @@ public class HandwritingInitiator {
        return null;
    }

    private static void requestFocusWithoutReveal(View view) {
    private void requestFocusWithoutReveal(View view) {
        if (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
            // the extended handwriting bounds), then we must calculate and set the cursor
            // position manually.
            view.getLocationInWindow(mTempLocation);
            int offset = editText.getOffsetForPosition(
                    mState.mStylusDownX - mTempLocation[0],
                    mState.mStylusDownY - mTempLocation[1]);
            editText.setSelection(offset);
        }
        if (view.getRevealOnFocusHint()) {
            view.setRevealOnFocusHint(false);
            view.requestFocus();
@@ -457,6 +472,10 @@ public class HandwritingInitiator {
            if (getViewHandwritingArea(connectedView, handwritingArea)
                    && isInHandwritingArea(handwritingArea, x, y, connectedView, isHover)
                    && shouldTriggerStylusHandwritingForView(connectedView)) {
                if (!isHover && mState != null) {
                    mState.mStylusDownWithinEditorBounds =
                            contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
                }
                return connectedView;
            }
        }
@@ -475,7 +494,12 @@ public class HandwritingInitiator {
            }

            final float distance = distance(handwritingArea, x, y);
            if (distance == 0f) return view;
            if (distance == 0f) {
                if (!isHover && mState != null) {
                    mState.mStylusDownWithinEditorBounds = true;
                }
                return view;
            }
            if (distance < minDistance) {
                minDistance = distance;
                bestCandidate = view;
@@ -657,6 +681,12 @@ public class HandwritingInitiator {
         */
        private boolean mExceedHandwritingSlop;

        /**
         * Whether the stylus down point of the MotionEvent sequence was within the editor's bounds
         * (not including the extended handwriting bounds).
         */
        private boolean mStylusDownWithinEditorBounds;

        /**
         * A view which has requested focus and is pending input connection creation. When an input
         * connection is created for the view, a handwriting session should be started for the view.
+19 −2
Original line number Diff line number Diff line
@@ -88,8 +88,8 @@ public class HandwritingInitiatorTest {
    }

    private HandwritingInitiator mHandwritingInitiator;
    private View mTestView1;
    private View mTestView2;
    private EditText mTestView1;
    private EditText mTestView2;
    private Context mContext;

    @Before
@@ -123,6 +123,9 @@ public class HandwritingInitiatorTest {

    @Test
    public void onTouchEvent_startHandwriting_when_stylusMoveOnce_withinHWArea() {
        mTestView1.setText("hello");
        when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);

        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
        final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
        final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -141,6 +144,9 @@ 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.
        verify(mTestView1, never()).setSelection(anyInt());
    }

    @Test
@@ -185,6 +191,9 @@ public class HandwritingInitiatorTest {

    @Test
    public void onTouchEvent_startHandwriting_when_stylusMove_withinExtendedHWArea() {
        mTestView1.setText("hello");
        when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);

        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
        final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2;
        final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2;
@@ -199,6 +208,9 @@ 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.
        verify(mTestView1).setSelection(4);
    }

    @Test
@@ -221,6 +233,8 @@ public class HandwritingInitiatorTest {

    @Test
    public void onTouchEvent_startHandwriting_inputConnectionBuilt_stylusMoveInExtendedHWArea() {
        mTestView1.setText("hello");
        when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
        // 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;
@@ -241,6 +255,9 @@ public class HandwritingInitiatorTest {
        // the stylus down point is closest to this view.
        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
        verify(mHandwritingInitiator).startHandwriting(mTestView1);
        // Since the stylus down point was outside the TextView's bounds, the handwriting initiator
        // sets the cursor position.
        verify(mTestView1).setSelection(4);
    }

    @Test
+15 −4
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package android.view.stylus;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

@@ -26,22 +29,23 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;

import androidx.test.platform.app.InstrumentationRegistry;

public class HandwritingTestUtil {
    public static View createView(Rect handwritingArea) {
    public static EditText createView(Rect handwritingArea) {
        return createView(handwritingArea, true /* autoHandwritingEnabled */,
                true /* isStylusHandwritingAvailable */);
    }

    public static View createView(Rect handwritingArea, boolean autoHandwritingEnabled,
    public static EditText createView(Rect handwritingArea, boolean autoHandwritingEnabled,
            boolean isStylusHandwritingAvailable) {
        return createView(handwritingArea, autoHandwritingEnabled, isStylusHandwritingAvailable,
                0, 0, 0, 0);
    }

    public static View createView(Rect handwritingArea, boolean autoHandwritingEnabled,
    public static EditText createView(Rect handwritingArea, boolean autoHandwritingEnabled,
            boolean isStylusHandwritingAvailable,
            float handwritingBoundsOffsetLeft, float handwritingBoundsOffsetTop,
            float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) {
@@ -68,7 +72,7 @@ public class HandwritingTestUtil {
            }
        };

        View view = spy(new View(context));
        EditText view = spy(new EditText(context));
        when(view.isAttachedToWindow()).thenReturn(true);
        when(view.isAggregatedVisible()).thenReturn(true);
        when(view.isStylusHandwritingAvailable()).thenReturn(isStylusHandwritingAvailable);
@@ -77,6 +81,13 @@ public class HandwritingTestUtil {
        when(view.getHandwritingBoundsOffsetTop()).thenReturn(handwritingBoundsOffsetTop);
        when(view.getHandwritingBoundsOffsetRight()).thenReturn(handwritingBoundsOffsetRight);
        when(view.getHandwritingBoundsOffsetBottom()).thenReturn(handwritingBoundsOffsetBottom);
        doAnswer(invocation -> {
            int[] outLocation = invocation.getArgument(0);
            outLocation[0] = handwritingArea.left;
            outLocation[1] = handwritingArea.top;
            return null;
        }).when(view).getLocationInWindow(any());
        when(view.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(0);
        view.setAutoHandwritingEnabled(autoHandwritingEnabled);
        parent.addView(view);
        return view;