Loading core/java/android/widget/Editor.java +112 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -461,6 +464,7 @@ public class Editor { private int mLineChangeSlopMin; private final AccessibilitySmartActions mA11ySmartActions; private InsertModeController mInsertModeController; Editor(TextView textView) { mTextView = textView; Loading Loading @@ -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); Loading Loading @@ -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. */ Loading core/java/android/widget/TextView.java +24 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -2534,7 +2535,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * @hide */ @VisibleForTesting @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public CharSequence getTransformed() { return mTransformed; } Loading Loading @@ -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<Class<? extends PreviewableHandwritingGesture>> previews = new ArraySet<>(); Loading Loading @@ -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); } Loading core/java/com/android/internal/inputmethod/EditableInputConnection.java +3 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading Loading
core/java/android/widget/Editor.java +112 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -461,6 +464,7 @@ public class Editor { private int mLineChangeSlopMin; private final AccessibilitySmartActions mA11ySmartActions; private InsertModeController mInsertModeController; Editor(TextView textView) { mTextView = textView; Loading Loading @@ -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); Loading Loading @@ -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. */ Loading
core/java/android/widget/TextView.java +24 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -2534,7 +2535,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * @hide */ @VisibleForTesting @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public CharSequence getTransformed() { return mTransformed; } Loading Loading @@ -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<Class<? extends PreviewableHandwritingGesture>> previews = new ArraySet<>(); Loading Loading @@ -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); } Loading
core/java/com/android/internal/inputmethod/EditableInputConnection.java +3 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading