From f1a41859d878dc1ec4378b827c8d148fa4995811 Mon Sep 17 00:00:00 2001 From: Haoyu Zhang Date: Tue, 17 Jan 2023 10:52:30 -0800 Subject: [PATCH] Support InsertModeGesture When IME send the InsertModeGesture, the TextView will set the special InsertModeTransformationMethod which creates some empty space for user to write text. Bug: 242089987 Test: atest TextViewHandwritingGestureTest Change-Id: I9877551cbd26318682312039b1d4852d33973034 --- core/java/android/widget/Editor.java | 112 ++++++++++++++++++ core/java/android/widget/TextView.java | 25 +++- .../inputmethod/EditableInputConnection.java | 3 + 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 95a42aaa8a43f..f2f455702f5db 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -73,10 +73,12 @@ import android.text.Spanned; import android.text.SpannedString; import android.text.StaticLayout; import android.text.TextUtils; +import android.text.method.InsertModeTransformationMethod; import android.text.method.KeyListener; import android.text.method.MetaKeyKeyListener; import android.text.method.MovementMethod; import android.text.method.OffsetMapping; +import android.text.method.TransformationMethod; import android.text.method.WordIterator; import android.text.style.EasyEditSpan; import android.text.style.SuggestionRangeSpan; @@ -136,6 +138,7 @@ import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; import com.android.internal.inputmethod.EditableInputConnection; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -461,6 +464,7 @@ public class Editor { private int mLineChangeSlopMin; private final AccessibilitySmartActions mA11ySmartActions; + private InsertModeController mInsertModeController; Editor(TextView textView) { mTextView = textView; @@ -2109,6 +2113,10 @@ public class Editor { } } + if (mInsertModeController != null) { + mInsertModeController.onDraw(canvas); + } + if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) { drawHardwareAccelerated(canvas, layout, highlightPaths, highlightPaints, selectionHighlight, selectionHighlightPaint, cursorOffsetVertical); @@ -8054,6 +8062,110 @@ public class Editor { } } + private static final class InsertModeController { + private final TextView mTextView; + private boolean mIsInsertModeActive; + private InsertModeTransformationMethod mInsertModeTransformationMethod; + private final Paint mHighlightPaint; + + InsertModeController(@NonNull TextView textView) { + mTextView = Objects.requireNonNull(textView); + mIsInsertModeActive = false; + mInsertModeTransformationMethod = null; + mHighlightPaint = new Paint(); + + // The highlight color is supposed to be 12% of the color primary40. We can't + // directly access Material 3 theme. But because Material 3 sets the colorPrimary to + // be primary40, here we hardcoded it to be 12% of colorPrimary. + final TypedValue typedValue = new TypedValue(); + mTextView.getContext().getTheme() + .resolveAttribute(R.attr.colorPrimary, typedValue, true); + final int colorPrimary = typedValue.data; + final int highlightColor = ColorUtils.setAlphaComponent(colorPrimary, + (int) (0.12f * Color.alpha(colorPrimary))); + mHighlightPaint.setColor(highlightColor); + } + + /** + * Enter insert mode. + * @param offset the index to set the cursor. + * @return true if the call is successful. false if a) it's already in the insert mode, + * b) it failed to enter the insert mode. + */ + boolean enterInsertMode(int offset) { + if (mIsInsertModeActive) return false; + + TransformationMethod oldTransformationMethod = + mTextView.getTransformationMethod(); + if (oldTransformationMethod instanceof OffsetMapping) { + // We can't support the case where the oldTransformationMethod is an OffsetMapping. + return false; + } + + final boolean isSingleLine = mTextView.isSingleLine(); + mInsertModeTransformationMethod = new InsertModeTransformationMethod(offset, + isSingleLine, oldTransformationMethod); + mTextView.setTransformationMethod(mInsertModeTransformationMethod); + Selection.setSelection((Spannable) mTextView.getText(), offset); + + mIsInsertModeActive = true; + return true; + } + + void exitInsertMode() { + if (!mIsInsertModeActive) return; + if (mInsertModeTransformationMethod == null + || mInsertModeTransformationMethod != mTextView.getTransformationMethod()) { + // If mInsertionModeTransformationMethod doesn't match the one on TextView, + // something else have changed the TextView's TransformationMethod while the + // insertion mode is active. We don't need to restore the oldTransformationMethod. + // TODO(265871733): support the case where setTransformationMethod is called in + // the insert mode. + mIsInsertModeActive = false; + return; + } + // Changing TransformationMethod will reset selection range to [0, 0), we need to + // manually restore the old selection range. + final int selectionStart = mTextView.getSelectionStart(); + final int selectionEnd = mTextView.getSelectionEnd(); + final TransformationMethod oldTransformationMethod = + mInsertModeTransformationMethod.getOldTransformationMethod(); + mTextView.setTransformationMethod(oldTransformationMethod); + Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); + mIsInsertModeActive = false; + } + + void onDraw(Canvas canvas) { + if (!mIsInsertModeActive) return; + final CharSequence transformedText = mTextView.getTransformed(); + if (transformedText instanceof InsertModeTransformationMethod.TransformedText) { + final Layout layout = mTextView.getLayout(); + if (layout == null) return; + final InsertModeTransformationMethod.TransformedText insertModeTransformedText = + ((InsertModeTransformationMethod.TransformedText) transformedText); + final int highlightStart = insertModeTransformedText.getHighlightStart(); + final int highlightEnd = insertModeTransformedText.getHighlightEnd(); + final Layout.SelectionRectangleConsumer consumer = + (left, top, right, bottom, textSelectionLayout) -> + canvas.drawRect(left, top, right, bottom, mHighlightPaint); + layout.getSelection(highlightStart, highlightEnd, consumer); + } + } + } + + boolean enterInsertMode(int offset) { + if (mInsertModeController == null) { + if (mTextView == null) return false; + mInsertModeController = new InsertModeController(mTextView); + } + return mInsertModeController.enterInsertMode(offset); + } + + void exitInsertMode() { + if (mInsertModeController == null) return; + mInsertModeController.exitInsertMode(); + } + /** * Initializes the nodeInfo with smart actions. */ diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 6ff808effadc1..6e36a0399dd7a 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -202,6 +202,7 @@ import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InsertGesture; +import android.view.inputmethod.InsertModeGesture; import android.view.inputmethod.JoinOrSplitGesture; import android.view.inputmethod.PreviewableHandwritingGesture; import android.view.inputmethod.RemoveSpaceGesture; @@ -2534,7 +2535,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * @hide */ - @VisibleForTesting + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public CharSequence getTransformed() { return mTransformed; } @@ -9620,6 +9621,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener gestures.add(InsertGesture.class); gestures.add(RemoveSpaceGesture.class); gestures.add(JoinOrSplitGesture.class); + gestures.add(InsertModeGesture.class); outAttrs.setSupportedHandwritingGestures(gestures); Set> previews = new ArraySet<>(); @@ -10169,6 +10171,27 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; } + /** @hide */ + public int performHandwritingInsertModeGesture(@NonNull InsertModeGesture gesture) { + final PointF insertPoint = + convertFromScreenToContentCoordinates(gesture.getInsertionPoint()); + final int line = getLineForHandwritingGesture(insertPoint); + final CancellationSignal cancellationSignal = gesture.getCancellationSignal(); + + // If no cancellationSignal is provided, don't enter the insert mode. + if (line == -1 || cancellationSignal == null) { + return handleGestureFailure(gesture); + } + + final int offset = mLayout.getOffsetForHorizontal(line, insertPoint.x); + + if (!mEditor.enterInsertMode(offset)) { + return InputConnection.HANDWRITING_GESTURE_RESULT_FAILED; + } + cancellationSignal.setOnCancelListener(() -> mEditor.exitInsertMode()); + return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; + } + private int handleGestureFailure(HandwritingGesture gesture) { return handleGestureFailure(gesture, /* isPreview= */ false); } diff --git a/core/java/com/android/internal/inputmethod/EditableInputConnection.java b/core/java/com/android/internal/inputmethod/EditableInputConnection.java index 8690e8d0d9d0b..3020d77a09bfc 100644 --- a/core/java/com/android/internal/inputmethod/EditableInputConnection.java +++ b/core/java/com/android/internal/inputmethod/EditableInputConnection.java @@ -44,6 +44,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InsertGesture; +import android.view.inputmethod.InsertModeGesture; import android.view.inputmethod.JoinOrSplitGesture; import android.view.inputmethod.PreviewableHandwritingGesture; import android.view.inputmethod.RemoveSpaceGesture; @@ -314,6 +315,8 @@ public final class EditableInputConnection extends BaseInputConnection result = mTextView.performHandwritingRemoveSpaceGesture((RemoveSpaceGesture) gesture); } else if (gesture instanceof JoinOrSplitGesture) { result = mTextView.performHandwritingJoinOrSplitGesture((JoinOrSplitGesture) gesture); + } else if (gesture instanceof InsertModeGesture) { + result = mTextView.performHandwritingInsertModeGesture((InsertModeGesture) gesture); } else { result = HANDWRITING_GESTURE_RESULT_UNSUPPORTED; }