Loading core/java/android/widget/Editor.java +6 −0 Original line number Diff line number Diff line Loading @@ -5440,6 +5440,9 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent ev) { if (!mTextView.isFromPrimePointer(ev, true)) { return true; } if (mFlagInsertionHandleGesturesEnabled && mFlagCursorDragFromAnywhereEnabled) { // Should only enable touch through when cursor drag is enabled. // Otherwise the insertion handle view cannot be moved. Loading Loading @@ -5908,6 +5911,9 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent event) { if (!mTextView.isFromPrimePointer(event, true)) { return true; } boolean superResult = super.onTouchEvent(event); switch (event.getActionMasked()) { Loading core/java/android/widget/TextView.java +46 −0 Original line number Diff line number Diff line Loading @@ -855,6 +855,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int mTextEditSuggestionContainerLayout; int mTextEditSuggestionHighlightStyle; private static final int NO_POINTER_ID = -1; /** * The prime (the 1st finger) pointer id which is used as a lock to prevent multi touch among * TextView and the handle views which are rendered on popup windows. */ private int mPrimePointerId = NO_POINTER_ID; /** * Whether the prime pointer is from the event delivered to selection handle or insertion * handle. */ private boolean mIsPrimePointerFromHandleView; /** * {@link EditText} specific data, created on demand when one of the Editor fields is used. * See {@link #createEditorIfNeeded()}. Loading Loading @@ -10886,6 +10899,36 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } /** * Called from onTouchEvent() to prevent the touches by secondary fingers. * Dragging on handles can revise cursor/selection, so can dragging on the text view. * This method is a lock to avoid processing multiple fingers on both text view and handles. * Note: multiple fingers on handles (e.g. 2 fingers on the 2 selection handles) should work. * * @param event The motion event that is being handled and carries the pointer info. * @param fromHandleView true if the event is delivered to selection handle or insertion * handle; false if this event is delivered to TextView. * @return Returns true to indicate that onTouchEvent() can continue processing the motion * event, otherwise false. * - Always returns true for the first finger. * - For secondary fingers, if the first or current finger is from TextView, returns false. * This is to make touch mutually exclusive between the TextView and the handles, but * not among the handles. */ boolean isFromPrimePointer(MotionEvent event, boolean fromHandleView) { if (mPrimePointerId == NO_POINTER_ID) { mPrimePointerId = event.getPointerId(0); mIsPrimePointerFromHandleView = fromHandleView; } else if (mPrimePointerId != event.getPointerId(0)) { return mIsPrimePointerFromHandleView && fromHandleView; } if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { mPrimePointerId = -1; } return true; } @Override public boolean onTouchEvent(MotionEvent event) { if (DEBUG_CURSOR) { Loading @@ -10894,6 +10937,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener MotionEvent.actionToString(event.getActionMasked()), event.getX(), event.getY()); } if (!isFromPrimePointer(event, false)) { return true; } final int action = event.getActionMasked(); if (mEditor != null) { Loading core/tests/coretests/src/android/widget/EditorCursorDragTest.java +59 −0 Original line number Diff line number Diff line Loading @@ -527,6 +527,47 @@ public class EditorCursorDragTest { .isEqualTo(2); } @Test public void testCursorDrag_multiTouch() throws Throwable { String text = "line1: This is the 1st line: A"; onView(withId(R.id.textview)).perform(replaceText(text)); TextView tv = mActivity.findViewById(R.id.textview); Editor editor = tv.getEditorForTesting(); final int startIndex = text.indexOf("1st line"); Layout layout = tv.getLayout(); final float cursorStartX = layout.getPrimaryHorizontal(startIndex) + tv.getTotalPaddingLeft(); final float cursorStartY = layout.getLineTop(1) + tv.getTotalPaddingTop(); // Taps to show the insertion handle. tapAtPoint(tv, cursorStartX, cursorStartY); onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex)); View handleView = editor.getInsertionController().getHandle(); // Taps & holds the insertion handle. long handleDownTime = sTicker.addAndGet(10_000); long eventTime = handleDownTime; dispatchTouchEvent(handleView, downEvent(handleView, handleDownTime, eventTime++, 0, 0)); // Tries to Drag the cursor, with the pointer id > 0 (meaning the 2nd finger). long cursorDownTime = eventTime++; dispatchTouchEvent(tv, obtainTouchEventWithPointerId( tv, MotionEvent.ACTION_DOWN, cursorDownTime, eventTime++, 1, cursorStartX - 50, cursorStartY)); dispatchTouchEvent(tv, obtainTouchEventWithPointerId( tv, MotionEvent.ACTION_MOVE, cursorDownTime, eventTime++, 1, cursorStartX - 100, cursorStartY)); dispatchTouchEvent(tv, obtainTouchEventWithPointerId( tv, MotionEvent.ACTION_UP, cursorDownTime, eventTime++, 1, cursorStartX - 100, cursorStartY)); // Checks the cursor drag doesn't work while the handle is being hold. onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex)); // Finger up on the insertion handle. dispatchTouchEvent(handleView, upEvent(handleView, handleDownTime, eventTime, 0, 0)); } @Test public void testCursorDrag_snapDistance() throws Throwable { String text = "line1: This is the 1st line: A\n" Loading Loading @@ -626,6 +667,24 @@ public class EditorCursorDragTest { return event; } private MotionEvent obtainTouchEventWithPointerId( View view, int action, long downTime, long eventTime, int pointerId, float x, float y) { Rect r = new Rect(); view.getBoundsOnScreen(r); float rawX = x + r.left; float rawY = y + r.top; MotionEvent.PointerCoords coordinates = new MotionEvent.PointerCoords(); coordinates.x = rawX; coordinates.y = rawY; MotionEvent event = MotionEvent.obtain( downTime, eventTime, action, 1, new int[] {pointerId}, new MotionEvent.PointerCoords[] {coordinates}, 0, 1f, 1f, 0, 0, 0, 0); view.toLocalMotionEvent(event); mMotionEvents.add(event); return event; } private MotionEvent obtainMouseEvent( View view, int action, long downTime, long eventTime, float x, float y) { MotionEvent event = obtainTouchEvent(view, action, downTime, eventTime, x, y); Loading Loading
core/java/android/widget/Editor.java +6 −0 Original line number Diff line number Diff line Loading @@ -5440,6 +5440,9 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent ev) { if (!mTextView.isFromPrimePointer(ev, true)) { return true; } if (mFlagInsertionHandleGesturesEnabled && mFlagCursorDragFromAnywhereEnabled) { // Should only enable touch through when cursor drag is enabled. // Otherwise the insertion handle view cannot be moved. Loading Loading @@ -5908,6 +5911,9 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent event) { if (!mTextView.isFromPrimePointer(event, true)) { return true; } boolean superResult = super.onTouchEvent(event); switch (event.getActionMasked()) { Loading
core/java/android/widget/TextView.java +46 −0 Original line number Diff line number Diff line Loading @@ -855,6 +855,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int mTextEditSuggestionContainerLayout; int mTextEditSuggestionHighlightStyle; private static final int NO_POINTER_ID = -1; /** * The prime (the 1st finger) pointer id which is used as a lock to prevent multi touch among * TextView and the handle views which are rendered on popup windows. */ private int mPrimePointerId = NO_POINTER_ID; /** * Whether the prime pointer is from the event delivered to selection handle or insertion * handle. */ private boolean mIsPrimePointerFromHandleView; /** * {@link EditText} specific data, created on demand when one of the Editor fields is used. * See {@link #createEditorIfNeeded()}. Loading Loading @@ -10886,6 +10899,36 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } /** * Called from onTouchEvent() to prevent the touches by secondary fingers. * Dragging on handles can revise cursor/selection, so can dragging on the text view. * This method is a lock to avoid processing multiple fingers on both text view and handles. * Note: multiple fingers on handles (e.g. 2 fingers on the 2 selection handles) should work. * * @param event The motion event that is being handled and carries the pointer info. * @param fromHandleView true if the event is delivered to selection handle or insertion * handle; false if this event is delivered to TextView. * @return Returns true to indicate that onTouchEvent() can continue processing the motion * event, otherwise false. * - Always returns true for the first finger. * - For secondary fingers, if the first or current finger is from TextView, returns false. * This is to make touch mutually exclusive between the TextView and the handles, but * not among the handles. */ boolean isFromPrimePointer(MotionEvent event, boolean fromHandleView) { if (mPrimePointerId == NO_POINTER_ID) { mPrimePointerId = event.getPointerId(0); mIsPrimePointerFromHandleView = fromHandleView; } else if (mPrimePointerId != event.getPointerId(0)) { return mIsPrimePointerFromHandleView && fromHandleView; } if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { mPrimePointerId = -1; } return true; } @Override public boolean onTouchEvent(MotionEvent event) { if (DEBUG_CURSOR) { Loading @@ -10894,6 +10937,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener MotionEvent.actionToString(event.getActionMasked()), event.getX(), event.getY()); } if (!isFromPrimePointer(event, false)) { return true; } final int action = event.getActionMasked(); if (mEditor != null) { Loading
core/tests/coretests/src/android/widget/EditorCursorDragTest.java +59 −0 Original line number Diff line number Diff line Loading @@ -527,6 +527,47 @@ public class EditorCursorDragTest { .isEqualTo(2); } @Test public void testCursorDrag_multiTouch() throws Throwable { String text = "line1: This is the 1st line: A"; onView(withId(R.id.textview)).perform(replaceText(text)); TextView tv = mActivity.findViewById(R.id.textview); Editor editor = tv.getEditorForTesting(); final int startIndex = text.indexOf("1st line"); Layout layout = tv.getLayout(); final float cursorStartX = layout.getPrimaryHorizontal(startIndex) + tv.getTotalPaddingLeft(); final float cursorStartY = layout.getLineTop(1) + tv.getTotalPaddingTop(); // Taps to show the insertion handle. tapAtPoint(tv, cursorStartX, cursorStartY); onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex)); View handleView = editor.getInsertionController().getHandle(); // Taps & holds the insertion handle. long handleDownTime = sTicker.addAndGet(10_000); long eventTime = handleDownTime; dispatchTouchEvent(handleView, downEvent(handleView, handleDownTime, eventTime++, 0, 0)); // Tries to Drag the cursor, with the pointer id > 0 (meaning the 2nd finger). long cursorDownTime = eventTime++; dispatchTouchEvent(tv, obtainTouchEventWithPointerId( tv, MotionEvent.ACTION_DOWN, cursorDownTime, eventTime++, 1, cursorStartX - 50, cursorStartY)); dispatchTouchEvent(tv, obtainTouchEventWithPointerId( tv, MotionEvent.ACTION_MOVE, cursorDownTime, eventTime++, 1, cursorStartX - 100, cursorStartY)); dispatchTouchEvent(tv, obtainTouchEventWithPointerId( tv, MotionEvent.ACTION_UP, cursorDownTime, eventTime++, 1, cursorStartX - 100, cursorStartY)); // Checks the cursor drag doesn't work while the handle is being hold. onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex)); // Finger up on the insertion handle. dispatchTouchEvent(handleView, upEvent(handleView, handleDownTime, eventTime, 0, 0)); } @Test public void testCursorDrag_snapDistance() throws Throwable { String text = "line1: This is the 1st line: A\n" Loading Loading @@ -626,6 +667,24 @@ public class EditorCursorDragTest { return event; } private MotionEvent obtainTouchEventWithPointerId( View view, int action, long downTime, long eventTime, int pointerId, float x, float y) { Rect r = new Rect(); view.getBoundsOnScreen(r); float rawX = x + r.left; float rawY = y + r.top; MotionEvent.PointerCoords coordinates = new MotionEvent.PointerCoords(); coordinates.x = rawX; coordinates.y = rawY; MotionEvent event = MotionEvent.obtain( downTime, eventTime, action, 1, new int[] {pointerId}, new MotionEvent.PointerCoords[] {coordinates}, 0, 1f, 1f, 0, 0, 0, 0); view.toLocalMotionEvent(event); mMotionEvents.add(event); return event; } private MotionEvent obtainMouseEvent( View view, int action, long downTime, long eventTime, float x, float y) { MotionEvent event = obtainTouchEvent(view, action, downTime, eventTime, x, y); Loading