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

Commit 9c1983d4 authored by Shu Chen's avatar Shu Chen Committed by Android (Google) Code Review
Browse files

Merge "Implement touch through on InsertionHandleView."

parents 4d1830d1 38451194
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>