OSDN Git Service

Triple click to start paragraph based selection.
authorKeisuke Kuroyanagi <ksk@google.com>
Thu, 5 Nov 2015 09:51:00 +0000 (18:51 +0900)
committerKeisuke Kuroyanagi <ksk@google.com>
Thu, 10 Dec 2015 06:01:19 +0000 (06:01 +0000)
- Detect triple click in TextView#onTouchEvent.
- Select paragraph on triple click.
- Extend drag accelerator to support paragraph based
selection.

Bug: 19544351
Change-Id: I0a6752a0642a2c569b69a1fc2c0f49169a72844a

core/java/android/widget/Editor.java
core/java/android/widget/TextView.java

index ae9b3c4..eb76788 100644 (file)
@@ -231,6 +231,7 @@ public class Editor {
     boolean mCreatedWithASelection;
 
     boolean mDoubleTap = false;
+    boolean mTripleClick = false;
 
     private Runnable mInsertionActionModeRunnable;
 
@@ -770,20 +771,12 @@ public class Editor {
         return retOffset;
     }
 
-    /**
-     * Adjusts selection to the word under last touch offset. Return true if the operation was
-     * successfully performed.
-     */
-    private boolean selectCurrentWord() {
-        if (!mTextView.canSelectText()) {
-            return false;
-        }
-
+    private boolean needsToSelectAllToSelectWordOrParagraph() {
         if (mTextView.hasPasswordTransformationMethod()) {
             // Always select all on a password field.
             // Cut/copy menu entries are not available for passwords, but being able to select all
             // is however useful to delete or paste to replace the entire content.
-            return mTextView.selectAllText();
+            return true;
         }
 
         int inputType = mTextView.getInputType();
@@ -798,6 +791,21 @@ public class Editor {
                 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
                 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
                 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Adjusts selection to the word under last touch offset. Return true if the operation was
+     * successfully performed.
+     */
+    private boolean selectCurrentWord() {
+        if (!mTextView.canSelectText()) {
+            return false;
+        }
+
+        if (needsToSelectAllToSelectWordOrParagraph()) {
             return mTextView.selectAllText();
         }
 
@@ -840,6 +848,63 @@ public class Editor {
         return selectionEnd > selectionStart;
     }
 
+    /**
+     * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
+     * successfully performed.
+     */
+    private boolean selectCurrentParagraph() {
+        if (!mTextView.canSelectText()) {
+            return false;
+        }
+
+        if (needsToSelectAllToSelectWordOrParagraph()) {
+            return mTextView.selectAllText();
+        }
+
+        long lastTouchOffsets = getLastTouchOffsets();
+        final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
+        final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
+
+        final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
+        final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
+        final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
+        if (start < end) {
+            Selection.setSelection((Spannable) mTextView.getText(), start, end);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get the minimum range of paragraphs that contains startOffset and endOffset.
+     */
+    private long getParagraphsRange(int startOffset, int endOffset) {
+        final Layout layout = mTextView.getLayout();
+        if (layout == null) {
+            return TextUtils.packRangeInLong(-1, -1);
+        }
+        final CharSequence text = mTextView.getText();
+        int minLine = layout.getLineForOffset(startOffset);
+        // Search paragraph start.
+        while (minLine > 0) {
+            final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
+            if (text.charAt(prevLineEndOffset - 1) == '\n') {
+                break;
+            }
+            minLine--;
+        }
+        int maxLine = layout.getLineForOffset(endOffset);
+        // Search paragraph end.
+        while (maxLine < layout.getLineCount() - 1) {
+            final int lineEndOffset = layout.getLineEnd(maxLine);
+            if (text.charAt(lineEndOffset - 1) == '\n') {
+                break;
+            }
+            maxLine++;
+        }
+        return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
+    }
+
     void onLocaleChanged() {
         // Will be re-created on demand in getWordIterator with the proper new locale
         mWordIterator = null;
@@ -3911,13 +3976,13 @@ public class Editor {
 
             // Cancel the single tap delayed runnable.
             if (mInsertionActionModeRunnable != null
-                    && (mDoubleTap || isCursorInsideEasyCorrectionSpan())) {
+                    && (mDoubleTap || mTripleClick || isCursorInsideEasyCorrectionSpan())) {
                 mTextView.removeCallbacks(mInsertionActionModeRunnable);
             }
 
             // Prepare and schedule the single tap runnable to run exactly after the double tap
             // timeout has passed.
-            if (!mDoubleTap && !isCursorInsideEasyCorrectionSpan()
+            if (!mDoubleTap && !mTripleClick && !isCursorInsideEasyCorrectionSpan()
                     && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
                 if (mTextActionMode == null) {
                     if (mInsertionActionModeRunnable == null) {
@@ -4485,6 +4550,8 @@ public class Editor {
         private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
         // Word based selection by dragging. Enabled after long pressing or double tapping.
         private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
+        // Paragraph based selection by dragging. Enabled after mouse triple click.
+        private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
 
         SelectionModifierCursorController() {
             resetTouchOffsets();
@@ -4569,7 +4636,7 @@ public class Editor {
 
                         // Double tap detection
                         if (mGestureStayedInTapRegion) {
-                            if (mDoubleTap) {
+                            if (mDoubleTap || mTripleClick) {
                                 final float deltaX = eventX - mDownPositionX;
                                 final float deltaY = eventY - mDownPositionY;
                                 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
@@ -4581,7 +4648,11 @@ public class Editor {
                                         distanceSquared < doubleTapSlop * doubleTapSlop;
 
                                 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
-                                    selectCurrentWordAndStartDrag();
+                                    if (mDoubleTap) {
+                                        selectCurrentWordAndStartDrag();
+                                    } else if (mTripleClick) {
+                                        selectCurrentParagraphAndStartDrag();
+                                    }
                                     mDiscardNextActionUp = true;
                                 }
                             }
@@ -4690,10 +4761,32 @@ public class Editor {
                     case DRAG_ACCELERATOR_MODE_WORD:
                         updateWordBasedSelection(event);
                         break;
+                    case DRAG_ACCELERATOR_MODE_PARAGRAPH:
+                        updateParagraphBasedSelection(event);
+                        break;
                 }
             }
         }
 
+        /**
+         * If the TextView allows text selection, selects the current paragraph and starts a drag.
+         *
+         * @return true if the drag was started.
+         */
+        private boolean selectCurrentParagraphAndStartDrag() {
+            if (mInsertionActionModeRunnable != null) {
+                mTextView.removeCallbacks(mInsertionActionModeRunnable);
+            }
+            if (mTextActionMode != null) {
+                mTextActionMode.finish();
+            }
+            if (!selectCurrentParagraph()) {
+                return false;
+            }
+            enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
+            return true;
+        }
+
         private void updateCharacterBasedSelection(MotionEvent event) {
             final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
             Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset);
@@ -4754,6 +4847,18 @@ public class Editor {
             Selection.setSelection((Spannable) mTextView.getText(),
                     startOffset, offset);
         }
+
+        private void updateParagraphBasedSelection(MotionEvent event) {
+            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+
+            final int start = Math.min(offset, mStartOffset);
+            final int end = Math.max(offset, mStartOffset);
+            final long paragraphsRange = getParagraphsRange(start, end);
+            final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
+            final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
+            Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+        }
+
         /**
          * @param event
          */
index 94b75b7..61df736 100644 (file)
@@ -17,7 +17,6 @@
 package android.widget;
 
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
-
 import android.R;
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
@@ -115,6 +114,7 @@ import android.view.Choreographer;
 import android.view.DragEvent;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -8408,12 +8408,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
         final int action = event.getActionMasked();
 
         if (mEditor != null && action == MotionEvent.ACTION_DOWN) {
-            // Detect double tap and inform the Editor.
-            if (mFirstTouch && (SystemClock.uptimeMillis() - mLastTouchUpTime) <=
-                    ViewConfiguration.getDoubleTapTimeout()) {
-                mEditor.mDoubleTap = true;
-                mFirstTouch = false;
+            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+            // Detect double tap and triple click and inform the Editor.
+            if ((mFirstTouch || (mEditor.mDoubleTap && isMouse))
+                        && (SystemClock.uptimeMillis() - mLastTouchUpTime) <=
+                                ViewConfiguration.getDoubleTapTimeout()) {
+                if (mFirstTouch) {
+                    mEditor.mTripleClick = false;
+                    mEditor.mDoubleTap = true;
+                    mFirstTouch = false;
+                } else {
+                    mEditor.mTripleClick = true;
+                    mEditor.mDoubleTap = false;
+                    mFirstTouch = false;
+                }
             } else {
+                mEditor.mTripleClick = false;
                 mEditor.mDoubleTap = false;
                 mFirstTouch = true;
             }