Loading core/java/android/widget/Editor.java +18 −4 Original line number Diff line number Diff line Loading @@ -387,6 +387,7 @@ public class Editor { private final SuggestionHelper mSuggestionHelper = new SuggestionHelper(); private boolean mFlagCursorDragFromAnywhereEnabled; private float mCursorDragDirectionMinXYRatio; private boolean mFlagInsertionHandleGesturesEnabled; // Specifies whether the new magnifier (with fish-eye effect) is enabled. Loading Loading @@ -423,6 +424,11 @@ public class Editor { mFlagCursorDragFromAnywhereEnabled = AppGlobals.getIntCoreSetting( WidgetFlags.KEY_ENABLE_CURSOR_DRAG_FROM_ANYWHERE, WidgetFlags.ENABLE_CURSOR_DRAG_FROM_ANYWHERE_DEFAULT ? 1 : 0) != 0; final int cursorDragMinAngleFromVertical = AppGlobals.getIntCoreSetting( WidgetFlags.KEY_CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL, WidgetFlags.CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL_DEFAULT); mCursorDragDirectionMinXYRatio = EditorTouchState.getXYRatio( cursorDragMinAngleFromVertical); mFlagInsertionHandleGesturesEnabled = AppGlobals.getIntCoreSetting( WidgetFlags.KEY_ENABLE_INSERTION_HANDLE_GESTURES, WidgetFlags.ENABLE_INSERTION_HANDLE_GESTURES_DEFAULT ? 1 : 0) != 0; Loading @@ -432,6 +438,8 @@ public class Editor { if (TextView.DEBUG_CURSOR) { logCursor("Editor", "Cursor drag from anywhere is %s.", mFlagCursorDragFromAnywhereEnabled ? "enabled" : "disabled"); logCursor("Editor", "Cursor drag min angle from vertical is %d (= %f x/y ratio)", cursorDragMinAngleFromVertical, mCursorDragDirectionMinXYRatio); logCursor("Editor", "Insertion handle gestures is %s.", mFlagInsertionHandleGesturesEnabled ? "enabled" : "disabled"); logCursor("Editor", "New magnifier is %s.", Loading @@ -457,6 +465,11 @@ public class Editor { mFlagCursorDragFromAnywhereEnabled = enabled; } @VisibleForTesting public void setCursorDragMinAngleFromVertical(int degreesFromVertical) { mCursorDragDirectionMinXYRatio = EditorTouchState.getXYRatio(degreesFromVertical); } @VisibleForTesting public boolean getFlagInsertionHandleGesturesEnabled() { return mFlagInsertionHandleGesturesEnabled; Loading Loading @@ -6132,7 +6145,8 @@ public class Editor { && mTextView.getLayout() != null && mTextView.isFocused() && mTouchState.isMovedEnoughForDrag() && !mTouchState.isDragCloseToVertical()) { && (mTouchState.getInitialDragDirectionXYRatio() > mCursorDragDirectionMinXYRatio || mTouchState.isOnHandle())) { startCursorDrag(event); } break; Loading core/java/android/widget/EditorTouchState.java +46 −9 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ public class EditorTouchState { private boolean mMultiTapInSameArea; private boolean mMovedEnoughForDrag; private boolean mIsDragCloseToVertical; private float mInitialDragDirectionXYRatio; public float getLastDownX() { return mLastDownX; Loading Loading @@ -98,8 +98,23 @@ public class EditorTouchState { return mMovedEnoughForDrag; } public boolean isDragCloseToVertical() { return mIsDragCloseToVertical && !mIsOnHandle; /** * When {@link #isMovedEnoughForDrag()} is {@code true}, this function returns the x/y ratio for * the initial drag direction. Smaller values indicate that the direction is closer to vertical, * while larger values indicate that the direction is closer to horizontal. For example: * <ul> * <li>if the drag direction is exactly vertical, this returns 0 * <li>if the drag direction is exactly horizontal, this returns {@link Float#MAX_VALUE} * <li>if the drag direction is 45 deg from vertical, this returns 1 * <li>if the drag direction is 30 deg from vertical, this returns 0.58 (x delta is smaller * than y delta) * <li>if the drag direction is 60 deg from vertical, this returns 1.73 (x delta is bigger * than y delta) * </ul> * This function never returns negative values, regardless of the direction of the drag. */ public float getInitialDragDirectionXYRatio() { return mInitialDragDirectionXYRatio; } public void setIsOnHandle(boolean onHandle) { Loading Loading @@ -155,7 +170,7 @@ public class EditorTouchState { mLastDownY = event.getY(); mLastDownMillis = event.getEventTime(); mMovedEnoughForDrag = false; mIsDragCloseToVertical = false; mInitialDragDirectionXYRatio = 0.0f; } else if (action == MotionEvent.ACTION_UP) { if (TextView.DEBUG_CURSOR) { logCursor("EditorTouchState", "ACTION_UP"); Loading @@ -164,7 +179,7 @@ public class EditorTouchState { mLastUpY = event.getY(); mLastUpMillis = event.getEventTime(); mMovedEnoughForDrag = false; mIsDragCloseToVertical = false; mInitialDragDirectionXYRatio = 0.0f; } else if (action == MotionEvent.ACTION_MOVE) { if (!mMovedEnoughForDrag) { float deltaX = event.getX() - mLastDownX; Loading @@ -174,9 +189,8 @@ public class EditorTouchState { int touchSlop = config.getScaledTouchSlop(); mMovedEnoughForDrag = distanceSquared > touchSlop * touchSlop; if (mMovedEnoughForDrag) { // If the direction of the swipe motion is within 45 degrees of vertical, it is // considered a vertical drag. mIsDragCloseToVertical = Math.abs(deltaX) <= Math.abs(deltaY); mInitialDragDirectionXYRatio = (deltaY == 0) ? Float.MAX_VALUE : Math.abs(deltaX / deltaY); } } } else if (action == MotionEvent.ACTION_CANCEL) { Loading @@ -185,7 +199,7 @@ public class EditorTouchState { mMultiTapStatus = MultiTapStatus.NONE; mMultiTapInSameArea = false; mMovedEnoughForDrag = false; mIsDragCloseToVertical = false; mInitialDragDirectionXYRatio = 0.0f; } } Loading @@ -201,4 +215,27 @@ public class EditorTouchState { float distanceSquared = (deltaX * deltaX) + (deltaY * deltaY); return distanceSquared <= maxDistance * maxDistance; } /** * Returns the x/y ratio corresponding to the given angle relative to vertical. Smaller angle * values (ie, closer to vertical) will result in a smaller x/y ratio. For example: * <ul> * <li>if the angle is 45 deg, the ratio is 1 * <li>if the angle is 30 deg, the ratio is 0.58 (x delta is smaller than y delta) * <li>if the angle is 60 deg, the ratio is 1.73 (x delta is bigger than y delta) * </ul> * If the passed-in value is <= 0, this function returns 0. If the passed-in value is >= 90, * this function returns {@link Float#MAX_VALUE}. * * @see #getInitialDragDirectionXYRatio() */ public static float getXYRatio(int angleFromVerticalInDegrees) { if (angleFromVerticalInDegrees <= 0) { return 0.0f; } if (angleFromVerticalInDegrees >= 90) { return Float.MAX_VALUE; } return (float) Math.tan(Math.toRadians(angleFromVerticalInDegrees)); } } core/java/android/widget/WidgetFlags.java +22 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,28 @@ public final class WidgetFlags { */ public static final boolean ENABLE_CURSOR_DRAG_FROM_ANYWHERE_DEFAULT = true; /** * Threshold for the direction of a swipe gesture in order for it to be handled as a cursor drag * rather than a scroll. The direction angle of the swipe gesture must exceed this value in * order to trigger cursor drag; otherwise, the swipe will be assumed to be a scroll gesture. * The value units for this flag is degrees and the valid range is [0,90] inclusive. If a value * < 0 is set, 0 will be used instead; if a value > 90 is set, 90 will be used instead. */ public static final String CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL = "CursorControlFeature__min_angle_from_vertical_to_start_cursor_drag"; /** * The key used in app core settings for the flag * {@link #CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL}. */ public static final String KEY_CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL = "widget__min_angle_from_vertical_to_start_cursor_drag"; /** * Default value for the flag {@link #CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL}. */ public static final int CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL_DEFAULT = 45; /** * The flag of finger-to-cursor distance in DP for cursor dragging. * The value unit is DP and the range is {0..100}. If the value is out of range, the legacy Loading core/tests/coretests/src/android/widget/EditorCursorDragTest.java +32 −0 Original line number Diff line number Diff line Loading @@ -200,6 +200,38 @@ public class EditorCursorDragTest { onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index)); } @Test public void testCursorDrag_diagonal_thresholdConfig() throws Throwable { TextView tv = mActivity.findViewById(R.id.textview); Editor editor = tv.getEditorForTesting(); StringBuilder sb = new StringBuilder(); for (int i = 1; i <= 9; i++) { sb.append("here is some text").append(i).append("\n"); } sb.append(Strings.repeat("abcdefghij\n", 400)).append("Last"); String text = sb.toString(); onView(withId(R.id.textview)).perform(replaceText(text)); int index = text.indexOf("text9"); onView(withId(R.id.textview)).perform(clickOnTextAtIndex(index)); onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index)); // Configure the drag direction threshold to require the drag to be exactly horizontal. With // this set, a swipe that is slightly off horizontal should not trigger cursor drag. editor.setCursorDragMinAngleFromVertical(90); int startIdx = text.indexOf("5"); int endIdx = text.indexOf("here is some text3"); onView(withId(R.id.textview)).perform(dragOnText(startIdx, endIdx)); onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index)); // Configure the drag direction threshold to require the drag to be 45 degrees or more from // vertical. With this set, the same swipe gesture as above should now trigger cursor drag. editor.setCursorDragMinAngleFromVertical(45); onView(withId(R.id.textview)).perform(dragOnText(startIdx, endIdx)); onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(endIdx)); } @Test public void testCursorDrag_vertical_whenTextViewContentsFitOnScreen() throws Throwable { String text = "012345_aaa\n" Loading core/tests/coretests/src/android/widget/EditorTouchStateTest.java +91 −14 Original line number Diff line number Diff line Loading @@ -165,7 +165,7 @@ public class EditorTouchStateTest { long event2Time = 1001; MotionEvent event2 = moveEvent(event1Time, event2Time, 200f, 31f); mTouchState.update(event2, mConfig); assertDrag(mTouchState, 20f, 30f, 0, 0, false); assertDrag(mTouchState, 20f, 30f, 0, 0, 180f); // Simulate an ACTION_UP event with a delay that's longer than the double-tap timeout. long event3Time = 5000; Loading Loading @@ -280,7 +280,7 @@ public class EditorTouchStateTest { long event3Time = 1002; MotionEvent event3 = moveEvent(event3Time, event3Time, newX, newY); mTouchState.update(event3, mConfig); assertDrag(mTouchState, 20f, 30f, 0, 0, false); assertDrag(mTouchState, 20f, 30f, 0, 0, Float.MAX_VALUE); // Simulate an ACTION_UP event. long event4Time = 1003; Loading @@ -301,15 +301,15 @@ public class EditorTouchStateTest { long event2Time = 1002; MotionEvent event2 = moveEvent(event1Time, event2Time, 100f, 174f); mTouchState.update(event2, mConfig); assertDrag(mTouchState, 0f, 0f, 0, 0, true); assertDrag(mTouchState, 0f, 0f, 0, 0, 100f / 174f); // Simulate another ACTION_MOVE event that is horizontal from the original down event. // The value of `isDragCloseToVertical` should NOT change since it should only reflect the // initial direction of movement. // The drag direction ratio should NOT change since it should only reflect the initial // direction of movement. long event3Time = 1003; MotionEvent event3 = moveEvent(event1Time, event3Time, 200f, 0f); mTouchState.update(event3, mConfig); assertDrag(mTouchState, 0f, 0f, 0, 0, true); assertDrag(mTouchState, 0f, 0f, 0, 0, 100f / 174f); // Simulate an ACTION_UP event. long event4Time = 1004; Loading @@ -330,15 +330,15 @@ public class EditorTouchStateTest { long event2Time = 1002; MotionEvent event2 = moveEvent(event1Time, event2Time, 100f, 90f); mTouchState.update(event2, mConfig); assertDrag(mTouchState, 0f, 0f, 0, 0, false); assertDrag(mTouchState, 0f, 0f, 0, 0, 100f / 90f); // Simulate another ACTION_MOVE event that is vertical from the original down event. // The value of `isDragCloseToVertical` should NOT change since it should only reflect the // initial direction of movement. // The drag direction ratio should NOT change since it should only reflect the initial // direction of movement. long event3Time = 1003; MotionEvent event3 = moveEvent(event1Time, event3Time, 0f, 200f); mTouchState.update(event3, mConfig); assertDrag(mTouchState, 0f, 0f, 0, 0, false); assertDrag(mTouchState, 0f, 0f, 0, 0, 100f / 90f); // Simulate an ACTION_UP event. long event4Time = 1004; Loading Loading @@ -374,7 +374,7 @@ public class EditorTouchStateTest { long event2Time = 1002; MotionEvent event2 = moveEvent(event2Time, event2Time, 200f, 30f); mTouchState.update(event2, mConfig); assertDrag(mTouchState, 20f, 30f, 0, 0, false); assertDrag(mTouchState, 20f, 30f, 0, 0, Float.MAX_VALUE); // Simulate an ACTION_CANCEL event. long event3Time = 1003; Loading Loading @@ -411,6 +411,84 @@ public class EditorTouchStateTest { assertSingleTap(mTouchState, 22f, 33f, 20f, 30f); } @Test public void testGetXYRatio() throws Exception { doTestGetXYRatio(-1, 0.0f); doTestGetXYRatio(0, 0.0f); doTestGetXYRatio(30, 0.58f); doTestGetXYRatio(45, 1.0f); doTestGetXYRatio(60, 1.73f); doTestGetXYRatio(90, Float.MAX_VALUE); doTestGetXYRatio(91, Float.MAX_VALUE); } private void doTestGetXYRatio(int angleFromVerticalInDegrees, float expectedXYRatioRounded) { float result = EditorTouchState.getXYRatio(angleFromVerticalInDegrees); String msg = String.format( "%d deg should give an x/y ratio of %f; actual unrounded result is %f", angleFromVerticalInDegrees, expectedXYRatioRounded, result); float roundedResult = (result == 0.0f || result == Float.MAX_VALUE) ? result : Math.round(result * 100) / 100f; assertThat(msg, roundedResult, is(expectedXYRatioRounded)); } @Test public void testUpdate_dragDirection() throws Exception { // Simulate moving straight up. doTestDragDirection(100f, 100f, 100f, 50f, 0f); // Simulate moving straight down. doTestDragDirection(100f, 100f, 100f, 150f, 0f); // Simulate moving straight left. doTestDragDirection(100f, 100f, 50f, 100f, Float.MAX_VALUE); // Simulate moving straight right. doTestDragDirection(100f, 100f, 150f, 100f, Float.MAX_VALUE); // Simulate moving up and right, < 45 deg from vertical. doTestDragDirection(100f, 100f, 110f, 50f, 10f / 50f); // Simulate moving up and right, > 45 deg from vertical. doTestDragDirection(100f, 100f, 150f, 90f, 50f / 10f); // Simulate moving down and right, < 45 deg from vertical. doTestDragDirection(100f, 100f, 110f, 150f, 10f / 50f); // Simulate moving down and right, > 45 deg from vertical. doTestDragDirection(100f, 100f, 150f, 110f, 50f / 10f); // Simulate moving down and left, < 45 deg from vertical. doTestDragDirection(100f, 100f, 90f, 150f, 10f / 50f); // Simulate moving down and left, > 45 deg from vertical. doTestDragDirection(100f, 100f, 50f, 110f, 50f / 10f); // Simulate moving up and left, < 45 deg from vertical. doTestDragDirection(100f, 100f, 90f, 50f, 10f / 50f); // Simulate moving up and left, > 45 deg from vertical. doTestDragDirection(100f, 100f, 50f, 90f, 50f / 10f); } private void doTestDragDirection(float downX, float downY, float moveX, float moveY, float expectedInitialDragDirectionXYRatio) { EditorTouchState touchState = new EditorTouchState(); // Simulate an ACTION_DOWN event. long event1Time = 1001; MotionEvent event1 = downEvent(event1Time, event1Time, downX, downY); touchState.update(event1, mConfig); // Simulate an ACTION_MOVE event. long event2Time = 1002; MotionEvent event2 = moveEvent(event1Time, event2Time, moveX, moveY); touchState.update(event2, mConfig); String msg = String.format("(%.0f,%.0f)=>(%.0f,%.0f)", downX, downY, moveX, moveY); assertThat(msg, touchState.getInitialDragDirectionXYRatio(), is(expectedInitialDragDirectionXYRatio)); } private static MotionEvent downEvent(long downTime, long eventTime, float x, float y) { return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0); } Loading Loading @@ -441,7 +519,7 @@ public class EditorTouchStateTest { } private static void assertDrag(EditorTouchState touchState, float lastDownX, float lastDownY, float lastUpX, float lastUpY, boolean isDragCloseToVertical) { float lastDownY, float lastUpX, float lastUpY, float initialDragDirectionXYRatio) { assertThat(touchState.getLastDownX(), is(lastDownX)); assertThat(touchState.getLastDownY(), is(lastDownY)); assertThat(touchState.getLastUpX(), is(lastUpX)); Loading @@ -451,7 +529,7 @@ public class EditorTouchStateTest { assertThat(touchState.isMultiTap(), is(false)); assertThat(touchState.isMultiTapInSameArea(), is(false)); assertThat(touchState.isMovedEnoughForDrag(), is(true)); assertThat(touchState.isDragCloseToVertical(), is(isDragCloseToVertical)); assertThat(touchState.getInitialDragDirectionXYRatio(), is(initialDragDirectionXYRatio)); } private static void assertMultiTap(EditorTouchState touchState, Loading @@ -467,6 +545,5 @@ public class EditorTouchStateTest { || multiTapStatus == MultiTapStatus.TRIPLE_CLICK)); assertThat(touchState.isMultiTapInSameArea(), is(isMultiTapInSameArea)); assertThat(touchState.isMovedEnoughForDrag(), is(false)); assertThat(touchState.isDragCloseToVertical(), is(false)); } } Loading
core/java/android/widget/Editor.java +18 −4 Original line number Diff line number Diff line Loading @@ -387,6 +387,7 @@ public class Editor { private final SuggestionHelper mSuggestionHelper = new SuggestionHelper(); private boolean mFlagCursorDragFromAnywhereEnabled; private float mCursorDragDirectionMinXYRatio; private boolean mFlagInsertionHandleGesturesEnabled; // Specifies whether the new magnifier (with fish-eye effect) is enabled. Loading Loading @@ -423,6 +424,11 @@ public class Editor { mFlagCursorDragFromAnywhereEnabled = AppGlobals.getIntCoreSetting( WidgetFlags.KEY_ENABLE_CURSOR_DRAG_FROM_ANYWHERE, WidgetFlags.ENABLE_CURSOR_DRAG_FROM_ANYWHERE_DEFAULT ? 1 : 0) != 0; final int cursorDragMinAngleFromVertical = AppGlobals.getIntCoreSetting( WidgetFlags.KEY_CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL, WidgetFlags.CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL_DEFAULT); mCursorDragDirectionMinXYRatio = EditorTouchState.getXYRatio( cursorDragMinAngleFromVertical); mFlagInsertionHandleGesturesEnabled = AppGlobals.getIntCoreSetting( WidgetFlags.KEY_ENABLE_INSERTION_HANDLE_GESTURES, WidgetFlags.ENABLE_INSERTION_HANDLE_GESTURES_DEFAULT ? 1 : 0) != 0; Loading @@ -432,6 +438,8 @@ public class Editor { if (TextView.DEBUG_CURSOR) { logCursor("Editor", "Cursor drag from anywhere is %s.", mFlagCursorDragFromAnywhereEnabled ? "enabled" : "disabled"); logCursor("Editor", "Cursor drag min angle from vertical is %d (= %f x/y ratio)", cursorDragMinAngleFromVertical, mCursorDragDirectionMinXYRatio); logCursor("Editor", "Insertion handle gestures is %s.", mFlagInsertionHandleGesturesEnabled ? "enabled" : "disabled"); logCursor("Editor", "New magnifier is %s.", Loading @@ -457,6 +465,11 @@ public class Editor { mFlagCursorDragFromAnywhereEnabled = enabled; } @VisibleForTesting public void setCursorDragMinAngleFromVertical(int degreesFromVertical) { mCursorDragDirectionMinXYRatio = EditorTouchState.getXYRatio(degreesFromVertical); } @VisibleForTesting public boolean getFlagInsertionHandleGesturesEnabled() { return mFlagInsertionHandleGesturesEnabled; Loading Loading @@ -6132,7 +6145,8 @@ public class Editor { && mTextView.getLayout() != null && mTextView.isFocused() && mTouchState.isMovedEnoughForDrag() && !mTouchState.isDragCloseToVertical()) { && (mTouchState.getInitialDragDirectionXYRatio() > mCursorDragDirectionMinXYRatio || mTouchState.isOnHandle())) { startCursorDrag(event); } break; Loading
core/java/android/widget/EditorTouchState.java +46 −9 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ public class EditorTouchState { private boolean mMultiTapInSameArea; private boolean mMovedEnoughForDrag; private boolean mIsDragCloseToVertical; private float mInitialDragDirectionXYRatio; public float getLastDownX() { return mLastDownX; Loading Loading @@ -98,8 +98,23 @@ public class EditorTouchState { return mMovedEnoughForDrag; } public boolean isDragCloseToVertical() { return mIsDragCloseToVertical && !mIsOnHandle; /** * When {@link #isMovedEnoughForDrag()} is {@code true}, this function returns the x/y ratio for * the initial drag direction. Smaller values indicate that the direction is closer to vertical, * while larger values indicate that the direction is closer to horizontal. For example: * <ul> * <li>if the drag direction is exactly vertical, this returns 0 * <li>if the drag direction is exactly horizontal, this returns {@link Float#MAX_VALUE} * <li>if the drag direction is 45 deg from vertical, this returns 1 * <li>if the drag direction is 30 deg from vertical, this returns 0.58 (x delta is smaller * than y delta) * <li>if the drag direction is 60 deg from vertical, this returns 1.73 (x delta is bigger * than y delta) * </ul> * This function never returns negative values, regardless of the direction of the drag. */ public float getInitialDragDirectionXYRatio() { return mInitialDragDirectionXYRatio; } public void setIsOnHandle(boolean onHandle) { Loading Loading @@ -155,7 +170,7 @@ public class EditorTouchState { mLastDownY = event.getY(); mLastDownMillis = event.getEventTime(); mMovedEnoughForDrag = false; mIsDragCloseToVertical = false; mInitialDragDirectionXYRatio = 0.0f; } else if (action == MotionEvent.ACTION_UP) { if (TextView.DEBUG_CURSOR) { logCursor("EditorTouchState", "ACTION_UP"); Loading @@ -164,7 +179,7 @@ public class EditorTouchState { mLastUpY = event.getY(); mLastUpMillis = event.getEventTime(); mMovedEnoughForDrag = false; mIsDragCloseToVertical = false; mInitialDragDirectionXYRatio = 0.0f; } else if (action == MotionEvent.ACTION_MOVE) { if (!mMovedEnoughForDrag) { float deltaX = event.getX() - mLastDownX; Loading @@ -174,9 +189,8 @@ public class EditorTouchState { int touchSlop = config.getScaledTouchSlop(); mMovedEnoughForDrag = distanceSquared > touchSlop * touchSlop; if (mMovedEnoughForDrag) { // If the direction of the swipe motion is within 45 degrees of vertical, it is // considered a vertical drag. mIsDragCloseToVertical = Math.abs(deltaX) <= Math.abs(deltaY); mInitialDragDirectionXYRatio = (deltaY == 0) ? Float.MAX_VALUE : Math.abs(deltaX / deltaY); } } } else if (action == MotionEvent.ACTION_CANCEL) { Loading @@ -185,7 +199,7 @@ public class EditorTouchState { mMultiTapStatus = MultiTapStatus.NONE; mMultiTapInSameArea = false; mMovedEnoughForDrag = false; mIsDragCloseToVertical = false; mInitialDragDirectionXYRatio = 0.0f; } } Loading @@ -201,4 +215,27 @@ public class EditorTouchState { float distanceSquared = (deltaX * deltaX) + (deltaY * deltaY); return distanceSquared <= maxDistance * maxDistance; } /** * Returns the x/y ratio corresponding to the given angle relative to vertical. Smaller angle * values (ie, closer to vertical) will result in a smaller x/y ratio. For example: * <ul> * <li>if the angle is 45 deg, the ratio is 1 * <li>if the angle is 30 deg, the ratio is 0.58 (x delta is smaller than y delta) * <li>if the angle is 60 deg, the ratio is 1.73 (x delta is bigger than y delta) * </ul> * If the passed-in value is <= 0, this function returns 0. If the passed-in value is >= 90, * this function returns {@link Float#MAX_VALUE}. * * @see #getInitialDragDirectionXYRatio() */ public static float getXYRatio(int angleFromVerticalInDegrees) { if (angleFromVerticalInDegrees <= 0) { return 0.0f; } if (angleFromVerticalInDegrees >= 90) { return Float.MAX_VALUE; } return (float) Math.tan(Math.toRadians(angleFromVerticalInDegrees)); } }
core/java/android/widget/WidgetFlags.java +22 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,28 @@ public final class WidgetFlags { */ public static final boolean ENABLE_CURSOR_DRAG_FROM_ANYWHERE_DEFAULT = true; /** * Threshold for the direction of a swipe gesture in order for it to be handled as a cursor drag * rather than a scroll. The direction angle of the swipe gesture must exceed this value in * order to trigger cursor drag; otherwise, the swipe will be assumed to be a scroll gesture. * The value units for this flag is degrees and the valid range is [0,90] inclusive. If a value * < 0 is set, 0 will be used instead; if a value > 90 is set, 90 will be used instead. */ public static final String CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL = "CursorControlFeature__min_angle_from_vertical_to_start_cursor_drag"; /** * The key used in app core settings for the flag * {@link #CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL}. */ public static final String KEY_CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL = "widget__min_angle_from_vertical_to_start_cursor_drag"; /** * Default value for the flag {@link #CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL}. */ public static final int CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL_DEFAULT = 45; /** * The flag of finger-to-cursor distance in DP for cursor dragging. * The value unit is DP and the range is {0..100}. If the value is out of range, the legacy Loading
core/tests/coretests/src/android/widget/EditorCursorDragTest.java +32 −0 Original line number Diff line number Diff line Loading @@ -200,6 +200,38 @@ public class EditorCursorDragTest { onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index)); } @Test public void testCursorDrag_diagonal_thresholdConfig() throws Throwable { TextView tv = mActivity.findViewById(R.id.textview); Editor editor = tv.getEditorForTesting(); StringBuilder sb = new StringBuilder(); for (int i = 1; i <= 9; i++) { sb.append("here is some text").append(i).append("\n"); } sb.append(Strings.repeat("abcdefghij\n", 400)).append("Last"); String text = sb.toString(); onView(withId(R.id.textview)).perform(replaceText(text)); int index = text.indexOf("text9"); onView(withId(R.id.textview)).perform(clickOnTextAtIndex(index)); onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index)); // Configure the drag direction threshold to require the drag to be exactly horizontal. With // this set, a swipe that is slightly off horizontal should not trigger cursor drag. editor.setCursorDragMinAngleFromVertical(90); int startIdx = text.indexOf("5"); int endIdx = text.indexOf("here is some text3"); onView(withId(R.id.textview)).perform(dragOnText(startIdx, endIdx)); onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index)); // Configure the drag direction threshold to require the drag to be 45 degrees or more from // vertical. With this set, the same swipe gesture as above should now trigger cursor drag. editor.setCursorDragMinAngleFromVertical(45); onView(withId(R.id.textview)).perform(dragOnText(startIdx, endIdx)); onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(endIdx)); } @Test public void testCursorDrag_vertical_whenTextViewContentsFitOnScreen() throws Throwable { String text = "012345_aaa\n" Loading
core/tests/coretests/src/android/widget/EditorTouchStateTest.java +91 −14 Original line number Diff line number Diff line Loading @@ -165,7 +165,7 @@ public class EditorTouchStateTest { long event2Time = 1001; MotionEvent event2 = moveEvent(event1Time, event2Time, 200f, 31f); mTouchState.update(event2, mConfig); assertDrag(mTouchState, 20f, 30f, 0, 0, false); assertDrag(mTouchState, 20f, 30f, 0, 0, 180f); // Simulate an ACTION_UP event with a delay that's longer than the double-tap timeout. long event3Time = 5000; Loading Loading @@ -280,7 +280,7 @@ public class EditorTouchStateTest { long event3Time = 1002; MotionEvent event3 = moveEvent(event3Time, event3Time, newX, newY); mTouchState.update(event3, mConfig); assertDrag(mTouchState, 20f, 30f, 0, 0, false); assertDrag(mTouchState, 20f, 30f, 0, 0, Float.MAX_VALUE); // Simulate an ACTION_UP event. long event4Time = 1003; Loading @@ -301,15 +301,15 @@ public class EditorTouchStateTest { long event2Time = 1002; MotionEvent event2 = moveEvent(event1Time, event2Time, 100f, 174f); mTouchState.update(event2, mConfig); assertDrag(mTouchState, 0f, 0f, 0, 0, true); assertDrag(mTouchState, 0f, 0f, 0, 0, 100f / 174f); // Simulate another ACTION_MOVE event that is horizontal from the original down event. // The value of `isDragCloseToVertical` should NOT change since it should only reflect the // initial direction of movement. // The drag direction ratio should NOT change since it should only reflect the initial // direction of movement. long event3Time = 1003; MotionEvent event3 = moveEvent(event1Time, event3Time, 200f, 0f); mTouchState.update(event3, mConfig); assertDrag(mTouchState, 0f, 0f, 0, 0, true); assertDrag(mTouchState, 0f, 0f, 0, 0, 100f / 174f); // Simulate an ACTION_UP event. long event4Time = 1004; Loading @@ -330,15 +330,15 @@ public class EditorTouchStateTest { long event2Time = 1002; MotionEvent event2 = moveEvent(event1Time, event2Time, 100f, 90f); mTouchState.update(event2, mConfig); assertDrag(mTouchState, 0f, 0f, 0, 0, false); assertDrag(mTouchState, 0f, 0f, 0, 0, 100f / 90f); // Simulate another ACTION_MOVE event that is vertical from the original down event. // The value of `isDragCloseToVertical` should NOT change since it should only reflect the // initial direction of movement. // The drag direction ratio should NOT change since it should only reflect the initial // direction of movement. long event3Time = 1003; MotionEvent event3 = moveEvent(event1Time, event3Time, 0f, 200f); mTouchState.update(event3, mConfig); assertDrag(mTouchState, 0f, 0f, 0, 0, false); assertDrag(mTouchState, 0f, 0f, 0, 0, 100f / 90f); // Simulate an ACTION_UP event. long event4Time = 1004; Loading Loading @@ -374,7 +374,7 @@ public class EditorTouchStateTest { long event2Time = 1002; MotionEvent event2 = moveEvent(event2Time, event2Time, 200f, 30f); mTouchState.update(event2, mConfig); assertDrag(mTouchState, 20f, 30f, 0, 0, false); assertDrag(mTouchState, 20f, 30f, 0, 0, Float.MAX_VALUE); // Simulate an ACTION_CANCEL event. long event3Time = 1003; Loading Loading @@ -411,6 +411,84 @@ public class EditorTouchStateTest { assertSingleTap(mTouchState, 22f, 33f, 20f, 30f); } @Test public void testGetXYRatio() throws Exception { doTestGetXYRatio(-1, 0.0f); doTestGetXYRatio(0, 0.0f); doTestGetXYRatio(30, 0.58f); doTestGetXYRatio(45, 1.0f); doTestGetXYRatio(60, 1.73f); doTestGetXYRatio(90, Float.MAX_VALUE); doTestGetXYRatio(91, Float.MAX_VALUE); } private void doTestGetXYRatio(int angleFromVerticalInDegrees, float expectedXYRatioRounded) { float result = EditorTouchState.getXYRatio(angleFromVerticalInDegrees); String msg = String.format( "%d deg should give an x/y ratio of %f; actual unrounded result is %f", angleFromVerticalInDegrees, expectedXYRatioRounded, result); float roundedResult = (result == 0.0f || result == Float.MAX_VALUE) ? result : Math.round(result * 100) / 100f; assertThat(msg, roundedResult, is(expectedXYRatioRounded)); } @Test public void testUpdate_dragDirection() throws Exception { // Simulate moving straight up. doTestDragDirection(100f, 100f, 100f, 50f, 0f); // Simulate moving straight down. doTestDragDirection(100f, 100f, 100f, 150f, 0f); // Simulate moving straight left. doTestDragDirection(100f, 100f, 50f, 100f, Float.MAX_VALUE); // Simulate moving straight right. doTestDragDirection(100f, 100f, 150f, 100f, Float.MAX_VALUE); // Simulate moving up and right, < 45 deg from vertical. doTestDragDirection(100f, 100f, 110f, 50f, 10f / 50f); // Simulate moving up and right, > 45 deg from vertical. doTestDragDirection(100f, 100f, 150f, 90f, 50f / 10f); // Simulate moving down and right, < 45 deg from vertical. doTestDragDirection(100f, 100f, 110f, 150f, 10f / 50f); // Simulate moving down and right, > 45 deg from vertical. doTestDragDirection(100f, 100f, 150f, 110f, 50f / 10f); // Simulate moving down and left, < 45 deg from vertical. doTestDragDirection(100f, 100f, 90f, 150f, 10f / 50f); // Simulate moving down and left, > 45 deg from vertical. doTestDragDirection(100f, 100f, 50f, 110f, 50f / 10f); // Simulate moving up and left, < 45 deg from vertical. doTestDragDirection(100f, 100f, 90f, 50f, 10f / 50f); // Simulate moving up and left, > 45 deg from vertical. doTestDragDirection(100f, 100f, 50f, 90f, 50f / 10f); } private void doTestDragDirection(float downX, float downY, float moveX, float moveY, float expectedInitialDragDirectionXYRatio) { EditorTouchState touchState = new EditorTouchState(); // Simulate an ACTION_DOWN event. long event1Time = 1001; MotionEvent event1 = downEvent(event1Time, event1Time, downX, downY); touchState.update(event1, mConfig); // Simulate an ACTION_MOVE event. long event2Time = 1002; MotionEvent event2 = moveEvent(event1Time, event2Time, moveX, moveY); touchState.update(event2, mConfig); String msg = String.format("(%.0f,%.0f)=>(%.0f,%.0f)", downX, downY, moveX, moveY); assertThat(msg, touchState.getInitialDragDirectionXYRatio(), is(expectedInitialDragDirectionXYRatio)); } private static MotionEvent downEvent(long downTime, long eventTime, float x, float y) { return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0); } Loading Loading @@ -441,7 +519,7 @@ public class EditorTouchStateTest { } private static void assertDrag(EditorTouchState touchState, float lastDownX, float lastDownY, float lastUpX, float lastUpY, boolean isDragCloseToVertical) { float lastDownY, float lastUpX, float lastUpY, float initialDragDirectionXYRatio) { assertThat(touchState.getLastDownX(), is(lastDownX)); assertThat(touchState.getLastDownY(), is(lastDownY)); assertThat(touchState.getLastUpX(), is(lastUpX)); Loading @@ -451,7 +529,7 @@ public class EditorTouchStateTest { assertThat(touchState.isMultiTap(), is(false)); assertThat(touchState.isMultiTapInSameArea(), is(false)); assertThat(touchState.isMovedEnoughForDrag(), is(true)); assertThat(touchState.isDragCloseToVertical(), is(isDragCloseToVertical)); assertThat(touchState.getInitialDragDirectionXYRatio(), is(initialDragDirectionXYRatio)); } private static void assertMultiTap(EditorTouchState touchState, Loading @@ -467,6 +545,5 @@ public class EditorTouchStateTest { || multiTapStatus == MultiTapStatus.TRIPLE_CLICK)); assertThat(touchState.isMultiTapInSameArea(), is(isMultiTapInSameArea)); assertThat(touchState.isMovedEnoughForDrag(), is(false)); assertThat(touchState.isDragCloseToVertical(), is(false)); } }