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

Commit db2f631a authored by Haoyu Zhang's avatar Haoyu Zhang
Browse files

[Scribe] Support extended handwriting boundary

To improve the user experience the handwriting area can be larger than EditText's view boundary.
This CL extended the size of each EditText's handwriting area by 20dip.

Bug: 211764956
Test: atest FrameworksCoreTests:android.view.stylus.HandwritingInitiatorTest
Test: atest android.view.inputmethod.cts.StylusHandwritingTest
Change-Id: I5980be2f6ae28ee13d486747fe8a371b1b5f82e6
parent ca4e6d49
Loading
Loading
Loading
Loading
+7 −3
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.app.Instrumentation;
import android.content.Context;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import android.util.DisplayMetrics;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;

@@ -59,10 +60,13 @@ public class HandwritingInitiatorPerfTest {
    public void setup() {
        final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mContext = mInstrumentation.getTargetContext();
        ViewConfiguration viewConfiguration = ViewConfiguration.get(mContext);
        final ViewConfiguration viewConfiguration = ViewConfiguration.get(mContext);
        mTouchSlop = viewConfiguration.getScaledTouchSlop();
        InputMethodManager inputMethodManager = mContext.getSystemService(InputMethodManager.class);
        mHandwritingInitiator = new HandwritingInitiator(viewConfiguration, inputMethodManager);
        final InputMethodManager inputMethodManager =
                mContext.getSystemService(InputMethodManager.class);
        final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
        mHandwritingInitiator = new HandwritingInitiator(viewConfiguration, inputMethodManager,
                displayMetrics);
    }

    @Test
+106 −13
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package android.view;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.inputmethod.InputMethodManager;

import com.android.internal.annotations.VisibleForTesting;
@@ -49,6 +51,12 @@ import java.util.List;
 * @hide
 */
public class HandwritingInitiator {
    /** The amount of extra space added to handwriting in dip. */
    private static final int HANDWRITING_AREA_PADDING_DIP = 20;

    /** The amount of extra space added to handwriting in px. */
    private final float mHandwritingAreaPaddingPx;

    /**
     * The touchSlop from {@link ViewConfiguration} used to decide whether a pointer is considered
     * moving or stationary.
@@ -88,9 +96,13 @@ public class HandwritingInitiator {

    @VisibleForTesting
    public HandwritingInitiator(@NonNull ViewConfiguration viewConfiguration,
            @NonNull InputMethodManager inputMethodManager) {
            @NonNull InputMethodManager inputMethodManager, DisplayMetrics displayMetrics) {
        mTouchSlop = viewConfiguration.getScaledTouchSlop();
        mHandwritingTimeoutInMillis = ViewConfiguration.getLongPressTimeout();
        mHandwritingAreaPaddingPx = TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                HANDWRITING_AREA_PADDING_DIP,
                displayMetrics);
        mImm = inputMethodManager;
    }

@@ -250,7 +262,8 @@ public class HandwritingInitiator {
        }

        final Rect handwritingArea = getViewHandwritingArea(connectedView);
        if (contains(handwritingArea, mState.mStylusDownX, mState.mStylusDownY)) {
        if (containsInExtendedHandwritingArea(handwritingArea,
                mState.mStylusDownX, mState.mStylusDownY)) {
            startHandwriting(connectedView);
        } else {
            reset();
@@ -281,14 +294,21 @@ public class HandwritingInitiator {
     */
    @Nullable
    private View findBestCandidateView(float x, float y) {
        float minDistance = Float.MAX_VALUE;
        View bestCandidate = null;

        // If the connectedView is not null and do not set any handwriting area, it will check
        // whether the connectedView's boundary contains the initial stylus position. If true,
        // directly return the connectedView.
        final View connectedView = getConnectedView();
        if (connectedView != null && connectedView.isAutoHandwritingEnabled()) {
            final Rect handwritingArea = getViewHandwritingArea(connectedView);
            if (contains(handwritingArea, x, y)) {
                return connectedView;
            Rect handwritingArea = getViewHandwritingArea(connectedView);
            if (containsInExtendedHandwritingArea(handwritingArea, x, y)) {
                final float distance = distance(handwritingArea, x, y);
                if (distance == 0f) return connectedView;

                bestCandidate = connectedView;
                minDistance = distance;
            }
        }

@@ -297,18 +317,78 @@ public class HandwritingInitiator {
                mHandwritingAreasTracker.computeViewInfos();
        for (HandwritableViewInfo viewInfo : handwritableViewInfos) {
            final View view = viewInfo.getView();
            if (!view.isAutoHandwritingEnabled()) continue;
            if (contains(viewInfo.getHandwritingArea(), x, y)) {
                return viewInfo.getView();
            final Rect handwritingArea = viewInfo.getHandwritingArea();
            if (!containsInExtendedHandwritingArea(handwritingArea, x, y)) continue;

            final float distance = distance(handwritingArea, x, y);

            if (distance == 0f) return view;
            if (distance < minDistance) {
                minDistance = distance;
                bestCandidate = view;
            }
        }
        return null;
        return bestCandidate;
    }

    /**
     *  Return the square of the distance from point (x, y) to the given rect, which is mainly used
     *  for comparison. The distance is defined to be: the shortest distance between (x, y) to any
     *  point on rect. When (x, y) is contained by the rect, return 0f.
     */
    private static float distance(@NonNull Rect rect, float x, float y) {
        if (contains(rect, x, y, 0f, 0f, 0f, 0f)) {
            return 0f;
        }

        /* The distance between point (x, y) and rect, there are 2 basic cases:
         * a) The distance is the distance from (x, y) to the closest corner on rect.
         *                    o |     |
         *         ---+-----+---
         *            |     |
         *         ---+-----+---
         *            |     |
         * b) The distance is the distance from (x, y) to the closest edge on rect.
         *                      |  o  |
         *         ---+-----+---
         *            |     |
         *         ---+-----+---
         *            |     |
         * We define xDistance as following(similar for yDistance):
         *   If x is in [left, right) 0, else min(abs(x - left), abs(x - y))
         * For case a, sqrt(xDistance^2 + yDistance^2) is the final distance.
         * For case b, distance should be yDistance, which is also equal to
         * sqrt(xDistance^2 + yDistance^2) because xDistance is 0.
         */
        final float xDistance;
        if (x >= rect.left && x < rect.right) {
            xDistance = 0f;
        } else if (x < rect.left) {
            xDistance = rect.left - x;
        } else {
            xDistance = x - rect.right;
        }

        final float yDistance;
        if (y >= rect.top && y < rect.bottom) {
            yDistance = 0f;
        } else if (y < rect.top) {
            yDistance = rect.top - y;
        } else {
            yDistance = y - rect.bottom;
        }
        // We can omit sqrt here because we only need the distance for comparison.
        return xDistance * xDistance + yDistance * yDistance;
    }

    /**
     * Return the handwriting area of the given view, represented in the window's coordinate.
     * If the view didn't set any handwriting area, it will return the view's boundary.
     * It will return null if the view or its handwriting area is not visible.
     *
     * The handwriting area is clipped to its visible part.
     * Notice that the returned rectangle is the view's original handwriting area without the
     * view's handwriting area extends.
     */
    @Nullable
    private static Rect getViewHandwritingArea(@NonNull View view) {
@@ -329,11 +409,24 @@ public class HandwritingInitiator {
    }

    /**
     * Return true if the (x, y) is inside by the given {@link Rect}.
     * Return true if the (x, y) is inside by the given {@link Rect} extended by the View's
     * handwriting extends settings.
     */
    private boolean containsInExtendedHandwritingArea(@Nullable Rect handwritingArea,
            float x, float y) {
        if (handwritingArea == null) return false;
        return contains(handwritingArea, x, y, mHandwritingAreaPaddingPx, mHandwritingAreaPaddingPx,
                mHandwritingAreaPaddingPx, mHandwritingAreaPaddingPx);
    }

    /**
     * Return true if the (x, y) is inside by the given {@link Rect} extended by the given
     * extendLeft, extendTop, extendRight and extendBottom.
     */
    private boolean contains(@Nullable Rect rect, float x, float y) {
        if (rect == null) return false;
        return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
    private static boolean contains(@NonNull Rect rect, float x, float y,
            float extendLeft, float extendTop, float extendRight, float extendBottom) {
        return x >= rect.left - extendLeft && x < rect.right  + extendRight
                && y >= rect.top - extendTop && y < rect.bottom + extendBottom;
    }

    private boolean largerThanTouchSlop(float x1, float y1, float x2, float y2) {
+4 −2
Original line number Diff line number Diff line
@@ -907,8 +907,10 @@ public final class ViewRootImpl implements ViewParent,
                ? Choreographer.getSfInstance() : Choreographer.getInstance();
        mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
        mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
        mHandwritingInitiator = new HandwritingInitiator(mViewConfiguration,
                mContext.getSystemService(InputMethodManager.class));
        mHandwritingInitiator = new HandwritingInitiator(
                mViewConfiguration,
                mContext.getSystemService(InputMethodManager.class),
                context.getResources().getDisplayMetrics());

        String processorOverrideName = context.getResources().getString(
                                    R.string.config_inputEventCompatProcessorOverrideClassName);
+68 −4
Original line number Diff line number Diff line
@@ -34,6 +34,8 @@ import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.HandwritingInitiator;
import android.view.InputDevice;
import android.view.MotionEvent;
@@ -61,23 +63,32 @@ import org.junit.runner.RunWith;
public class HandwritingInitiatorTest {
    private static final int TOUCH_SLOP = 8;
    private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout();
    private static final int HANDWRITING_AREA_PADDING_DIP = 20;

    private static final Rect sHwArea = new Rect(100, 200, 500, 500);

    private HandwritingInitiator mHandwritingInitiator;
    private View mTestView;
    private Context mContext;
    private int mHandwritingAreaPaddingPx;

    @Before
    public void setup() {
        final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mContext = mInstrumentation.getTargetContext();
        ViewConfiguration viewConfiguration = mock(ViewConfiguration.class);
        final ViewConfiguration viewConfiguration = mock(ViewConfiguration.class);
        when(viewConfiguration.getScaledTouchSlop()).thenReturn(TOUCH_SLOP);


        final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
        InputMethodManager inputMethodManager = mContext.getSystemService(InputMethodManager.class);
        mHandwritingInitiator =
                spy(new HandwritingInitiator(viewConfiguration, inputMethodManager));
        mHandwritingInitiator = spy(new HandwritingInitiator(viewConfiguration, inputMethodManager,
                displayMetrics));

        mHandwritingAreaPaddingPx = Math.round(TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                HANDWRITING_AREA_PADDING_DIP,
                displayMetrics));
        mTestView = createView(sHwArea, true);
        mHandwritingInitiator.updateHandwritingAreasForView(mTestView);
    }
@@ -126,6 +137,24 @@ public class HandwritingInitiatorTest {
        verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView);
    }

    @Test
    public void onTouchEvent_startHandwriting_when_stylusMove_withinExtendedHWArea() {
        mHandwritingInitiator.onInputConnectionCreated(mTestView);
        final int x1 = sHwArea.left - mHandwritingAreaPaddingPx / 2;
        final int y1 = sHwArea.top - mHandwritingAreaPaddingPx / 2;
        MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
        mHandwritingInitiator.onTouchEvent(stylusEvent1);

        final int x2 = x1 + TOUCH_SLOP * 2;
        final int y2 = y1;

        MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
        mHandwritingInitiator.onTouchEvent(stylusEvent2);

        // Stylus movement within extended HandwritingArea should trigger IMM.startHandwriting once.
        verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView);
    }

    @Test
    public void onTouchEvent_startHandwriting_inputConnectionBuiltAfterStylusMove() {
        final int x1 = (sHwArea.left + sHwArea.right) / 2;
@@ -144,6 +173,24 @@ public class HandwritingInitiatorTest {
        verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView);
    }

    @Test
    public void onTouchEvent_startHandwriting_inputConnectionBuilt_stylusMoveInExtendedHWArea() {
        final int x1 = sHwArea.right + mHandwritingAreaPaddingPx / 2;
        final int y1 = sHwArea.bottom + mHandwritingAreaPaddingPx / 2;
        MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
        mHandwritingInitiator.onTouchEvent(stylusEvent1);

        final int x2 = x1 + TOUCH_SLOP * 2;
        final int y2 = y1;
        MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
        mHandwritingInitiator.onTouchEvent(stylusEvent2);

        // InputConnection is created after stylus movement.
        mHandwritingInitiator.onInputConnectionCreated(mTestView);

        verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView);
    }

    @Test
    public void onTouchEvent_notStartHandwriting_when_stylusTap_withinHWArea() {
        mHandwritingInitiator.onInputConnectionCreated(mTestView);
@@ -212,6 +259,23 @@ public class HandwritingInitiatorTest {
        verify(mTestView, times(1)).requestFocus();
    }

    @Test
    public void onTouchEvent_focusView_stylusMoveOnce_withinExtendedHWArea() {
        final int x1 = sHwArea.left - mHandwritingAreaPaddingPx / 2;
        final int y1 = sHwArea.top - mHandwritingAreaPaddingPx / 2;
        MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
        mHandwritingInitiator.onTouchEvent(stylusEvent1);

        final int x2 = x1 + TOUCH_SLOP * 2;
        final int y2 = y1;

        MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
        mHandwritingInitiator.onTouchEvent(stylusEvent2);

        // HandwritingInitiator will request focus for the registered view.
        verify(mTestView, times(1)).requestFocus();
    }

    @Test
    public void autoHandwriting_whenDisabled_wontStartHW() {
        View mockView = createView(sHwArea, false);