OSDN Git Service

Use TEXT_HANDLE_MOVE in TextView behind the flag
authorYohei Yukawa <yukawa@google.com>
Tue, 20 Jun 2017 01:27:34 +0000 (18:27 -0700)
committerYohei Yukawa <yukawa@google.com>
Tue, 20 Jun 2017 01:27:34 +0000 (18:27 -0700)
This CL enables TextView to trigger
HapticFeedbackConstants.TEXT_HANDLE_MOVE on devices that explicitly
enable this feature with resource overlay.  This CL should have no
behavior change and no performance impact by default.

Since the use case of HapticFeedbackConstants.TEXT_HANDLE_MOVE is
when the user is manually moving the text insertion/selection handle
on the touch screen, it is intentional that text handle move
triggered by hardware keyboard, mouse, TextView APIs, IME APIs, and
any other internal API calls do not trigger the haptic feedback
even if the feature is enabled with resource overlay.

Bug: 62454887
Test: Manually done as follows.
  * Not triggered on cursor move by mouse
  * Not triggered on cursor move by hardware keyboard
  * Not triggered on cursor move by IME
  * Triggered on cursor move by touch screen
Change-Id: I5e78aafb065378ca88ba39ec507b121c8baa3631

core/java/android/widget/Editor.java
core/res/res/values/config.xml
core/res/res/values/symbols.xml

index 45e5f8a..fa1bd91 100644 (file)
@@ -83,6 +83,7 @@ import android.view.DisplayListCanvas;
 import android.view.DragAndDropPermissions;
 import android.view.DragEvent;
 import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
 import android.view.InputDevice;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -176,6 +177,8 @@ public class Editor {
     private boolean mInsertionControllerEnabled;
     private boolean mSelectionControllerEnabled;
 
+    private final boolean mHapticTextHandleEnabled;
+
     // Used to highlight a word when it is corrected by the IME
     private CorrectionHighlighter mCorrectionHighlighter;
 
@@ -320,6 +323,8 @@ public class Editor {
         // Synchronize the filter list, which places the undo input filter at the end.
         mTextView.setFilters(mTextView.getFilters());
         mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
+        mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_enableHapticTextHandle);
     }
 
     ParcelableParcel saveInstanceState() {
@@ -4253,7 +4258,7 @@ public class Editor {
             mNumberPreviousOffsets++;
         }
 
-        private void filterOnTouchUp() {
+        private void filterOnTouchUp(boolean fromTouchScreen) {
             final long now = SystemClock.uptimeMillis();
             int i = 0;
             int index = mPreviousOffsetIndex;
@@ -4265,7 +4270,7 @@ public class Editor {
 
             if (i > 0 && i < iMax
                     && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
-                positionAtCursorOffset(mPreviousOffsets[index], false);
+                positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
             }
         }
 
@@ -4282,7 +4287,7 @@ public class Editor {
         public void invalidate() {
             super.invalidate();
             if (isShowing()) {
-                positionAtCursorOffset(getCurrentCursorOffset(), true);
+                positionAtCursorOffset(getCurrentCursorOffset(), true, false);
             }
         };
 
@@ -4301,7 +4306,7 @@ public class Editor {
 
             // Make sure the offset is always considered new, even when focusing at same position
             mPreviousOffset = -1;
-            positionAtCursorOffset(getCurrentCursorOffset(), false);
+            positionAtCursorOffset(getCurrentCursorOffset(), false, false);
         }
 
         protected void dismiss() {
@@ -4338,7 +4343,7 @@ public class Editor {
 
         protected abstract void updateSelection(int offset);
 
-        public abstract void updatePosition(float x, float y);
+        protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
 
         protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
             return layout.isRtlCharAt(offset);
@@ -4357,8 +4362,11 @@ public class Editor {
          * @param offset Cursor offset. Must be in [-1, length].
          * @param forceUpdatePosition whether to force update the position.  This should be true
          * when If the parent has been scrolled, for example.
+         * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
+         * touch screen.
          */
-        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition) {
+        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
+                boolean fromTouchScreen) {
             // A HandleView relies on the layout, which may be nulled by external methods
             Layout layout = mTextView.getLayout();
             if (layout == null) {
@@ -4372,6 +4380,9 @@ public class Editor {
             if (offsetChanged || forceUpdatePosition) {
                 if (offsetChanged) {
                     updateSelection(offset);
+                    if (fromTouchScreen && mHapticTextHandleEnabled) {
+                        mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
+                    }
                     addPositionToTouchUpFilter(offset);
                 }
                 final int line = layout.getLineForOffset(offset);
@@ -4404,7 +4415,7 @@ public class Editor {
         @Override
         public void updatePosition(int parentPositionX, int parentPositionY,
                 boolean parentPositionChanged, boolean parentScrolled) {
-            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
+            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
             if (parentPositionChanged || mPositionHasChanged) {
                 if (mIsDragging) {
                     // Update touchToWindow offset in case of parent scrolling while dragging
@@ -4516,12 +4527,13 @@ public class Editor {
                             xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
                     final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
 
-                    updatePosition(newPosX, newPosY);
+                    updatePosition(newPosX, newPosY,
+                            ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
                     break;
                 }
 
                 case MotionEvent.ACTION_UP:
-                    filterOnTouchUp();
+                    filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
                     mIsDragging = false;
                     updateDrawable();
                     break;
@@ -4702,7 +4714,7 @@ public class Editor {
         }
 
         @Override
-        public void updatePosition(float x, float y) {
+        protected void updatePosition(float x, float y, boolean fromTouchScreen) {
             Layout layout = mTextView.getLayout();
             int offset;
             if (layout != null) {
@@ -4715,7 +4727,7 @@ public class Editor {
             } else {
                 offset = -1;
             }
-            positionAtCursorOffset(offset, false);
+            positionAtCursorOffset(offset, false, fromTouchScreen);
             if (mTextActionMode != null) {
                 invalidateActionMode();
             }
@@ -4806,12 +4818,13 @@ public class Editor {
         }
 
         @Override
-        public void updatePosition(float x, float y) {
+        protected void updatePosition(float x, float y, boolean fromTouchScreen) {
             final Layout layout = mTextView.getLayout();
             if (layout == null) {
                 // HandleView will deal appropriately in positionAtCursorOffset when
                 // layout is null.
-                positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y));
+                positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
+                        fromTouchScreen);
                 return;
             }
 
@@ -4854,12 +4867,12 @@ public class Editor {
                 // to the current position.
                 mLanguageDirectionChanged = true;
                 mTouchWordDelta = 0.0f;
-                positionAndAdjustForCrossingHandles(offset);
+                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
                 return;
             } else if (mLanguageDirectionChanged && !isLvlBoundary) {
                 // We've just moved past the boundary so update the position. After this we can
                 // figure out if the user is expanding or shrinking to go by word or character.
-                positionAndAdjustForCrossingHandles(offset);
+                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
                 mTouchWordDelta = 0.0f;
                 mLanguageDirectionChanged = false;
                 return;
@@ -4893,7 +4906,7 @@ public class Editor {
                     final int nextOffset = (atRtl == isStartHandle())
                             ? layout.getOffsetToRightOf(mPreviousOffset)
                             : layout.getOffsetToLeftOf(mPreviousOffset);
-                    positionAndAdjustForCrossingHandles(nextOffset);
+                    positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
                     return;
                 }
             }
@@ -4972,14 +4985,15 @@ public class Editor {
 
             if (positionCursor) {
                 mPreviousLineTouched = currLine;
-                positionAndAdjustForCrossingHandles(offset);
+                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
             }
             mPrevX = x;
         }
 
         @Override
-        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition) {
-            super.positionAtCursorOffset(offset, forceUpdatePosition);
+        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
+                boolean fromTouchScreen) {
+            super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
             mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
         }
 
@@ -4995,7 +5009,7 @@ public class Editor {
             return superResult;
         }
 
-        private void positionAndAdjustForCrossingHandles(int offset) {
+        private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
             final int anotherHandleOffset =
                     isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
             if ((isStartHandle() && offset >= anotherHandleOffset)
@@ -5020,14 +5034,14 @@ public class Editor {
                         } else {
                             offset = TextUtils.unpackRangeEndFromLong(range);
                         }
-                        positionAtCursorOffset(offset, false);
+                        positionAtCursorOffset(offset, false, fromTouchScreen);
                         return;
                     }
                 }
                 // Handles can not cross and selection is at least one character.
                 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
             }
-            positionAtCursorOffset(offset, false);
+            positionAtCursorOffset(offset, false, fromTouchScreen);
         }
 
         private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
@@ -5470,7 +5484,8 @@ public class Editor {
 
         private void updateCharacterBasedSelection(MotionEvent event) {
             final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
-            Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset);
+            updateSelectionInternal(mStartOffset, offset,
+                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
         }
 
         private void updateWordBasedSelection(MotionEvent event) {
@@ -5528,7 +5543,8 @@ public class Editor {
                 }
             }
             mLineSelectionIsOn = currLine;
-            Selection.setSelection((Spannable) mTextView.getText(), startOffset, offset);
+            updateSelectionInternal(startOffset, offset,
+                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
         }
 
         private void updateParagraphBasedSelection(MotionEvent event) {
@@ -5539,7 +5555,19 @@ public class Editor {
             final long paragraphsRange = getParagraphsRange(start, end);
             final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
             final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
+            updateSelectionInternal(selectionStart, selectionEnd,
+                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+        }
+
+        private void updateSelectionInternal(int selectionStart, int selectionEnd,
+                boolean fromTouchScreen) {
+            final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
+                    && ((mTextView.getSelectionStart() != selectionStart)
+                            || (mTextView.getSelectionEnd() != selectionEnd));
             Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+            if (performHapticFeedback) {
+                mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
+            }
         }
 
         /**
index 4a000b9..96b30da 100644 (file)
          fading edges is prohibitively expensive on most GPUs. -->
     <bool name="config_ui_enableFadingMarquee">false</bool>
 
+    <!-- Enables or disables haptic effect when the text insertion/selection handle is moved
+         manually by the user. Off by default, since the expected haptic feedback may not be
+         available on some devices. -->
+    <bool name="config_enableHapticTextHandle">false</bool>
+
     <!-- Whether dialogs should close automatically when the user touches outside
          of them.  This should not normally be modified. -->
     <bool name="config_closeDialogWhenTouchOutside">true</bool>
index 7cee6d6..d60c8d2 100644 (file)
   <java-symbol type="bool" name="config_syncstorageengine_masterSyncAutomatically" />
   <java-symbol type="bool" name="config_telephony_use_own_number_for_voicemail" />
   <java-symbol type="bool" name="config_ui_enableFadingMarquee" />
+  <java-symbol type="bool" name="config_enableHapticTextHandle" />
   <java-symbol type="bool" name="config_use_strict_phone_number_comparation" />
   <java-symbol type="bool" name="config_single_volume" />
   <java-symbol type="bool" name="config_voice_capable" />