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

Commit 38451194 authored by Shu Chen's avatar Shu Chen
Browse files

Implement touch through on InsertionHandleView.

So that the logic of double-tap & long-press to select words can be shared to InsertionHandleView.
Note:
  - This requires to work with the cursor dragging feature, otherwise the InsertionHandleView cannot move.
  - InsertionHandleView needs to handle touch up events to toggle the insertion menu as expected.
  - The event coordinates need to be modified in order to make the touches on handle equivalent to touches on text at cursor.

Bug: 145547052,145535934
Test: m -j & manual test & automated tests
        atest frameworksCoreTests:EditorCursorDragTest
        atest FrameworksCoreTests:TextViewActivityTest
Change-Id: Ic025700eb06addd67a2abcfff97eaf0e8a196718
parent c33182c3
Loading
Loading
Loading
Loading
+159 −4
Original line number Diff line number Diff line
@@ -387,7 +387,7 @@ public class Editor {

    // Specifies whether the cursor control feature set is enabled.
    // This can only be true if the text view is editable.
    private final boolean mCursorControlEnabled;
    private boolean mCursorControlEnabled;

    Editor(TextView textView) {
        mTextView = textView;
@@ -411,6 +411,16 @@ public class Editor {
        }
    }

    @VisibleForTesting
    public void setCursorControlEnabled(boolean enabled) {
        mCursorControlEnabled = enabled;
    }

    @VisibleForTesting
    public boolean getCursorControlEnabled() {
        return mCursorControlEnabled;
    }

    ParcelableParcel saveInstanceState() {
        ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
        Parcel parcel = state.getParcel();
@@ -1204,7 +1214,7 @@ public class Editor {
        }
        // Long press in empty space moves cursor and starts the insertion action mode.
        if (!handled && !isPositionOnText(mTouchState.getLastDownX(), mTouchState.getLastDownY())
                && mInsertionControllerEnabled) {
                && !mTouchState.isOnHandle() && mInsertionControllerEnabled) {
            final int offset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
                    mTouchState.getLastDownY());
            Selection.setSelection((Spannable) mTextView.getText(), offset);
@@ -5135,6 +5145,37 @@ public class Editor {
        private float mLastDownRawX, mLastDownRawY;
        private Runnable mHider;

        // Members for fake-dismiss effect in touch through mode.
        // It is to make InsertionHandleView can receive the MOVE/UP events after calling dismiss(),
        // which could happen in case of long-press (making selection will dismiss the insertion
        // handle).

        // Whether the finger is down and hasn't been up yet.
        private boolean mIsTouchDown = false;
        // Whether the popup window is in the invisible state and will be dismissed when finger up.
        private boolean mPendingDismissOnUp = false;
        // The alpha value of the drawable.
        private final int mDrawableOpacity = 255;

        // Members for toggling the insertion menu in touch through mode.

        // The coordinate for the touch down event, which is used for transforming the coordinates
        // of the events to the text view.
        private float mTouchDownX;
        private float mTouchDownY;
        // The cursor offset when touch down. This is to detect whether the cursor is moved when
        // finger move/up.
        private int mOffsetDown;
        // Whether the cursor offset has been changed on the move/up events.
        private boolean mOffsetChanged;
        // Whether it is in insertion action mode when finger down.
        private boolean mIsInActionMode;
        // The timestamp for the last up event, which is used for double tap detection.
        private long mLastUpTime;
        // The text height of the font of the text view, which is used to calculate the Y coordinate
        // of the touch through events.
        private float mTextHeight;

        public InsertionHandleView(Drawable drawable) {
            super(drawable, drawable, com.android.internal.R.id.insertion_handle);
        }
@@ -5190,6 +5231,11 @@ public class Editor {

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (mCursorControlEnabled && FLAG_ENABLE_CURSOR_DRAG) {
                // Should only enable touch through when cursor drag is enabled.
                // Otherwise the insertion handle view cannot be moved.
                return touchThrough(ev);
            }
            final boolean result = super.onTouchEvent(ev);

            switch (ev.getActionMasked()) {
@@ -5235,6 +5281,115 @@ public class Editor {
            return result;
        }

        // Handles the touch events in touch through mode.
        private boolean touchThrough(MotionEvent ev) {
            final int actionType = ev.getActionMasked();
            switch (actionType) {
                case MotionEvent.ACTION_DOWN:
                    mIsTouchDown = true;
                    mOffsetChanged = false;
                    mOffsetDown = mTextView.getSelectionStart();
                    mTouchDownX = ev.getX();
                    mTouchDownY = ev.getY();
                    mIsInActionMode = mTextActionMode != null;
                    if (ev.getEventTime() - mLastUpTime < ViewConfiguration.getDoubleTapTimeout()) {
                        stopTextActionMode();  // Avoid crash when double tap and drag backwards.
                    }
                    final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics();
                    mTextHeight = fontMetrics.descent - fontMetrics.ascent;
                    mTouchState.setIsOnHandle(true);
                    break;
                case MotionEvent.ACTION_UP:
                    mLastUpTime = ev.getEventTime();
                    break;
            }
            // Performs the touch through by forward the events to the text view.
            boolean ret = mTextView.onTouchEvent(transformEventForTouchThrough(ev));

            if (actionType == MotionEvent.ACTION_UP || actionType == MotionEvent.ACTION_CANCEL) {
                mIsTouchDown = false;
                if (mPendingDismissOnUp) {
                    dismiss();
                }
                mTouchState.setIsOnHandle(false);
            }

            // Checks for cursor offset change.
            if (!mOffsetChanged) {
                int start = mTextView.getSelectionStart();
                int end = mTextView.getSelectionEnd();
                if (start != end || mOffsetDown != start) {
                    mOffsetChanged = true;
                }
            }

            // Toggling the insertion action mode on finger up.
            if (!mOffsetChanged && actionType == MotionEvent.ACTION_UP) {
                if (mIsInActionMode) {
                    stopTextActionMode();
                } else {
                    startInsertionActionMode();
                }
            }
            return ret;
        }

        private MotionEvent transformEventForTouchThrough(MotionEvent ev) {
            // Transforms the touch events to screen coordinates.
            // And also shift up to make the hit point is on the text.
            // Note:
            //  - The revised X should reflect the distance to the horizontal center of touch down.
            //  - The revised Y should be at the top of the text.
            Matrix m = new Matrix();
            m.setTranslate(ev.getRawX() - ev.getX() + (getMeasuredWidth() >> 1) - mTouchDownX,
                    ev.getRawY() - ev.getY() - mTouchDownY - mTextHeight);
            ev.transform(m);
            // Transforms the touch events to text view coordinates.
            mTextView.toLocalMotionEvent(ev);
            if (TextView.DEBUG_CURSOR) {
                logCursor("InsertionHandleView#transformEventForTouchThrough",
                        "Touch through: %d, (%f, %f)",
                        ev.getAction(), ev.getX(), ev.getY());
            }
            return ev;
        }

        @Override
        public boolean isShowing() {
            if (mPendingDismissOnUp) {
                return false;
            }
            return super.isShowing();
        }

        @Override
        public void show() {
            super.show();
            mPendingDismissOnUp = false;
            mDrawable.setAlpha(mDrawableOpacity);
        }

        @Override
        public void dismiss() {
            if (mIsTouchDown) {
                if (TextView.DEBUG_CURSOR) {
                    logCursor("InsertionHandleView#dismiss",
                            "Suppressed the real dismiss, only become invisible");
                }
                mPendingDismissOnUp = true;
                mDrawable.setAlpha(0);
            } else {
                super.dismiss();
                mPendingDismissOnUp = false;
            }
        }

        @Override
        protected void updateDrawable(final boolean updateDrawableWhenDragging) {
            super.updateDrawable(updateDrawableWhenDragging);
            mDrawable.setAlpha(mDrawableOpacity);
        }

        @Override
        public int getCurrentCursorOffset() {
            return mTextView.getSelectionStart();
@@ -6039,8 +6194,8 @@ public class Editor {
                                eventX, eventY);

                        // Double tap detection
                        if (mTouchState.isMultiTapInSameArea()
                                && (isMouse || isPositionOnText(eventX, eventY))) {
                        if (mTouchState.isMultiTapInSameArea() && (isMouse
                                || mTouchState.isOnHandle() || isPositionOnText(eventX, eventY))) {
                            if (TextView.DEBUG_CURSOR) {
                                logCursor("SelectionModifierCursorController: onTouchEvent",
                                        "ACTION_DOWN: select and start drag");
+10 −1
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ public class EditorTouchState {
    private long mLastDownMillis;
    private float mLastUpX, mLastUpY;
    private long mLastUpMillis;
    private boolean mIsOnHandle;

    @IntDef({MultiTapStatus.NONE, MultiTapStatus.FIRST_TAP, MultiTapStatus.DOUBLE_TAP,
            MultiTapStatus.TRIPLE_CLICK})
@@ -98,7 +99,15 @@ public class EditorTouchState {
    }

    public boolean isDragCloseToVertical() {
        return mIsDragCloseToVertical;
        return mIsDragCloseToVertical && !mIsOnHandle;
    }

    public void setIsOnHandle(boolean onHandle) {
        mIsOnHandle = onHandle;
    }

    public boolean isOnHandle() {
        return mIsOnHandle;
    }

    /**
+109 −0
Original line number Diff line number Diff line
@@ -27,9 +27,13 @@ import static android.widget.espresso.FloatingToolbarEspressoUtils.sleepForFloat
import static android.widget.espresso.TextViewActions.Handle;
import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
import static android.widget.espresso.TextViewActions.doubleClickOnTextAtIndex;
import static android.widget.espresso.TextViewActions.doubleTapAndDragHandle;
import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText;
import static android.widget.espresso.TextViewActions.doubleTapHandle;
import static android.widget.espresso.TextViewActions.dragHandle;
import static android.widget.espresso.TextViewActions.longPressAndDragOnText;
import static android.widget.espresso.TextViewActions.longPressAndDragHandle;
import static android.widget.espresso.TextViewActions.longPressHandle;
import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
import static android.widget.espresso.TextViewAssertions.doesNotHaveStyledText;
import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
@@ -510,6 +514,111 @@ public class TextViewActivityTest {
        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("f")));
    }

    @Test
    public void testInsertionHandle_touchThrough() {
        final TextView textView = mActivity.findViewById(R.id.textview);
        boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled();
        boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG;
        textView.getEditorForTesting().setCursorControlEnabled(true);
        Editor.FLAG_ENABLE_CURSOR_DRAG = true;

        testInsertionHandle();
        testInsertionHandle_multiLine();

        textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled);
        Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled;
    }

    @Test
    public void testInsertionHandle_longPressToSelect() {
        // This test only makes sense when Cursor Control flag is enabled.
        final TextView textView = mActivity.findViewById(R.id.textview);
        boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled();
        boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG;
        textView.getEditorForTesting().setCursorControlEnabled(true);
        Editor.FLAG_ENABLE_CURSOR_DRAG = true;

        final String text = "hello the world";
        onView(withId(R.id.textview)).perform(replaceText(text));

        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));

        onHandleView(com.android.internal.R.id.insertion_handle).perform(longPressHandle(textView));
        onView(withId(R.id.textview)).check(hasSelection("world"));

        textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled);
        Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled;
    }

    @Test
    public void testInsertionHandle_longPressAndDragToSelect() {
        // This test only makes sense when Cursor Control flag is enabled.
        final TextView textView = mActivity.findViewById(R.id.textview);
        boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled();
        boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG;
        textView.getEditorForTesting().setCursorControlEnabled(true);
        Editor.FLAG_ENABLE_CURSOR_DRAG = true;

        final String text = "hello the world";
        onView(withId(R.id.textview)).perform(replaceText(text));

        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));

        onHandleView(com.android.internal.R.id.insertion_handle)
                .perform(longPressAndDragHandle(textView, Handle.INSERTION, text.indexOf('t')));
        onView(withId(R.id.textview)).check(hasSelection("the world"));

        textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled);
        Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled;
    }

    @Test
    public void testInsertionHandle_doubleTapToSelect() {
        // This test only makes sense when Cursor Control flag is enabled.
        final TextView textView = mActivity.findViewById(R.id.textview);
        boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled();
        boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG;
        textView.getEditorForTesting().setCursorControlEnabled(true);
        Editor.FLAG_ENABLE_CURSOR_DRAG = true;

        final String text = "hello the world";
        onView(withId(R.id.textview)).perform(replaceText(text));

        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));

        onHandleView(com.android.internal.R.id.insertion_handle).perform(doubleTapHandle(textView));
        onView(withId(R.id.textview)).check(hasSelection("world"));

        textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled);
        Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled;
    }

    @Test
    public void testInsertionHandle_doubleTapAndDragToSelect() {
        // This test only makes sense when Cursor Control flag is enabled.
        final TextView textView = mActivity.findViewById(R.id.textview);
        boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled();
        boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG;
        textView.getEditorForTesting().setCursorControlEnabled(true);
        Editor.FLAG_ENABLE_CURSOR_DRAG = true;

        final String text = "hello the world";
        onView(withId(R.id.textview)).perform(replaceText(text));

        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));

        onHandleView(com.android.internal.R.id.insertion_handle)
                .perform(doubleTapAndDragHandle(textView, Handle.INSERTION, text.indexOf('t')));
        onView(withId(R.id.textview)).check(hasSelection("the world"));

        textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled);
        Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled;
    }

    @Test
    public void testSelectionHandles() {
        final String text = "abcd efg hijk lmn";
+80 −0
Original line number Diff line number Diff line
@@ -198,6 +198,86 @@ public final class TextViewActions {
                        TextView.class));
    }

    /**
     * Returns an action that long presses then drags on handle from the current position to
     * endIndex on the TextView.<br>
     * <br>
     * View constraints:
     * <ul>
     * <li>must be a TextView's drag-handle displayed on screen
     * <ul>
     *
     * @param textView TextView the handle is on
     * @param handleType Type of the handle
     * @param endIndex The index of the TextView's text to end the drag at
     */
    public static ViewAction longPressAndDragHandle(TextView textView, Handle handleType,
            int endIndex) {
        return actionWithAssertions(
                new DragAction(
                        DragAction.Drag.LONG_PRESS,
                        new CurrentHandleCoordinates(textView),
                        new HandleCoordinates(textView, handleType, endIndex, true),
                        Press.FINGER,
                        Editor.HandleView.class));
    }

    /**
     * Returns an action that long presses on the current handle.<br>
     * <br>
     * View constraints:
     * <ul>
     * <li>must be a TextView's drag-handle displayed on screen
     * <ul>
     *
     * @param textView TextView the handle is on
     */
    public static ViewAction longPressHandle(TextView textView) {
        return actionWithAssertions(
                new ViewClickAction(Tap.LONG, new CurrentHandleCoordinates(textView),
                        Press.FINGER));
    }

    /**
     * Returns an action that double tap then drags on handle from the current position to
     * endIndex on the TextView.<br>
     * <br>
     * View constraints:
     * <ul>
     * <li>must be a TextView's drag-handle displayed on screen
     * <ul>
     *
     * @param textView TextView the handle is on
     * @param handleType Type of the handle
     * @param endIndex The index of the TextView's text to end the drag at
     */
    public static ViewAction doubleTapAndDragHandle(TextView textView, Handle handleType,
            int endIndex) {
        return actionWithAssertions(
                new DragAction(
                        DragAction.Drag.DOUBLE_TAP,
                        new CurrentHandleCoordinates(textView),
                        new HandleCoordinates(textView, handleType, endIndex, true),
                        Press.FINGER,
                        Editor.HandleView.class));
    }

    /**
     * Returns an action that double tap on the current handle.<br>
     * <br>
     * View constraints:
     * <ul>
     * <li>must be a TextView's drag-handle displayed on screen
     * <ul>
     *
     * @param textView TextView the handle is on
     */
    public static ViewAction doubleTapHandle(TextView textView) {
        return actionWithAssertions(
                new ViewClickAction(Tap.DOUBLE, new CurrentHandleCoordinates(textView),
                        Press.FINGER));
    }

    /**
     * Returns an action that double taps then drags on text from startIndex to endIndex on the
     * TextView.<br>