Loading core/java/android/view/HandwritingInitiator.java +61 −41 Original line number Diff line number Diff line Loading @@ -60,7 +60,7 @@ public class HandwritingInitiator { */ private final long mHandwritingTimeoutInMillis; private final State mState = new State(); private State mState; private final HandwritingAreaTracker mHandwritingAreasTracker = new HandwritingAreaTracker(); /** The reference to the View that currently has the input connection. */ Loading @@ -86,17 +86,27 @@ public class HandwritingInitiator { /** * Notify the HandwritingInitiator that a new MotionEvent has arrived. * This method is non-block, and the event passed to this method should be dispatched to the * View tree as usual. If HandwritingInitiator triggers the handwriting mode, an fabricated * ACTION_CANCEL event will be sent to the ViewRootImpl. * @param motionEvent the stylus MotionEvent. * * <p>The return value indicates whether the event has been fully handled by the * HandwritingInitiator and should not be dispatched to the view tree. This will be true for * ACTION_MOVE events from a stylus gesture after handwriting mode has been initiated, in order * to suppress other actions such as scrolling. * * <p>If HandwritingInitiator triggers the handwriting mode, a fabricated ACTION_CANCEL event * will be sent to the ViewRootImpl. * * @param motionEvent the stylus {@link MotionEvent} * @return true if the event has been fully handled by the {@link HandwritingInitiator} and * should not be dispatched to the {@link View} tree, or false if the event should be dispatched * to the {@link View} tree as usual */ @VisibleForTesting public void onTouchEvent(@NonNull MotionEvent motionEvent) { public boolean onTouchEvent(@NonNull MotionEvent motionEvent) { final int maskedAction = motionEvent.getActionMasked(); switch (maskedAction) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: mState = null; final int actionIndex = motionEvent.getActionIndex(); final int toolType = motionEvent.getToolType(actionIndex); // TOOL_TYPE_ERASER is also from stylus. This indicates that the user is holding Loading @@ -104,42 +114,44 @@ public class HandwritingInitiator { if (toolType != MotionEvent.TOOL_TYPE_STYLUS && toolType != MotionEvent.TOOL_TYPE_ERASER) { // The motion event is not from a stylus event, ignore it. return; return false; } if (!mImm.isStylusHandwritingAvailable()) { return false; } mState.mStylusPointerId = motionEvent.getPointerId(actionIndex); mState.mStylusDownTimeInMillis = motionEvent.getEventTime(); mState.mStylusDownX = motionEvent.getX(actionIndex); mState.mStylusDownY = motionEvent.getY(actionIndex); mState.mStylusDownCandidateView = new WeakReference<>( findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY)); mState.mShouldInitHandwriting = true; mState.mExceedHandwritingSlop = false; mState = new State(motionEvent); break; case MotionEvent.ACTION_POINTER_UP: final int pointerId = motionEvent.getPointerId(motionEvent.getActionIndex()); if (pointerId != mState.mStylusPointerId) { if (mState == null || pointerId != mState.mStylusPointerId) { // ACTION_POINTER_UP is from another stylus pointer, ignore the event. return; return false; } // Deliberately fall through. case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // If it's ACTION_CANCEL or ACTION_UP, all the pointers go up. There is no need to // check whether the stylus we are tracking goes up. if (mState != null) { mState.mShouldInitHandwriting = false; break; } return false; case MotionEvent.ACTION_MOVE: if (mState == null) { return false; } // Either we've already tried to initiate handwriting, or the ongoing MotionEvent // sequence is considered to be tap, long-click or other gestures. if (!mState.mShouldInitHandwriting || mState.mExceedHandwritingSlop) { return; return mState.mHasInitiatedHandwriting; } final long timeElapsed = motionEvent.getEventTime() - mState.mStylusDownTimeInMillis; if (timeElapsed > mHandwritingTimeoutInMillis) { mState.mShouldInitHandwriting = false; return; return mState.mHasInitiatedHandwriting; } final int pointerIndex = motionEvent.findPointerIndex(mState.mStylusPointerId); Loading @@ -147,13 +159,8 @@ public class HandwritingInitiator { final float y = motionEvent.getY(pointerIndex); if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) { mState.mExceedHandwritingSlop = true; View candidateView = mState.mStylusDownCandidateView.get(); if (candidateView == null || !candidateView.isAttachedToWindow()) { // If there was no candidate view found in the stylus down event, or if that // candidate view is no longer attached, search again for a candidate view. candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); } View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); if (candidateView != null) { if (candidateView == getConnectedView()) { startHandwriting(candidateView); Loading @@ -162,7 +169,9 @@ public class HandwritingInitiator { } } } return mState.mHasInitiatedHandwriting; } return false; } @Nullable Loading Loading @@ -195,7 +204,7 @@ public class HandwritingInitiator { } else { mConnectedView = new WeakReference<>(view); mConnectionCount = 1; if (mState.mShouldInitHandwriting) { if (mState != null && mState.mShouldInitHandwriting) { tryStartHandwriting(); } } Loading Loading @@ -259,6 +268,7 @@ public class HandwritingInitiator { @VisibleForTesting public void startHandwriting(@NonNull View view) { mImm.startStylusHandwriting(view); mState.mHasInitiatedHandwriting = true; mState.mShouldInitHandwriting = false; } Loading Loading @@ -438,28 +448,38 @@ public class HandwritingInitiator { * b) If the MotionEvent sequence is considered to be tap, long-click or other gestures. * This boolean will be set to false, and it won't request to start handwriting. */ private boolean mShouldInitHandwriting = false; private boolean mShouldInitHandwriting; /** * Whether handwriting mode has already been initiated for the current MotionEvent sequence. */ private boolean mHasInitiatedHandwriting; /** * Whether the current ongoing stylus MotionEvent sequence already exceeds the * handwriting slop. * It's used for the case where the stylus exceeds handwriting slop before the target View * built InputConnection. */ private boolean mExceedHandwritingSlop = false; private boolean mExceedHandwritingSlop; /** The pointer id of the stylus pointer that is being tracked. */ private int mStylusPointerId = -1; private final int mStylusPointerId; /** The time stamp when the stylus pointer goes down. */ private long mStylusDownTimeInMillis = -1; private final long mStylusDownTimeInMillis; /** The initial location where the stylus pointer goes down. */ private float mStylusDownX = Float.NaN; private float mStylusDownY = Float.NaN; /** * The best candidate view to initialize handwriting mode based on the initial location * where the stylus pointer goes down, or null if the location was not within any candidate * view's handwriting area. */ private WeakReference<View> mStylusDownCandidateView = new WeakReference<>(null); private final float mStylusDownX; private final float mStylusDownY; private State(MotionEvent motionEvent) { final int actionIndex = motionEvent.getActionIndex(); mStylusPointerId = motionEvent.getPointerId(actionIndex); mStylusDownTimeInMillis = motionEvent.getEventTime(); mStylusDownX = motionEvent.getX(actionIndex); mStylusDownY = motionEvent.getY(actionIndex); mShouldInitHandwriting = true; mHasInitiatedHandwriting = false; mExceedHandwritingSlop = false; } } /** The helper method to check if the given view is still active for handwriting. */ Loading core/java/android/view/ViewRootImpl.java +4 −2 Original line number Diff line number Diff line Loading @@ -6554,11 +6554,13 @@ public final class ViewRootImpl implements ViewParent, private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; mHandwritingInitiator.onTouchEvent(event); boolean handled = mHandwritingInitiator.onTouchEvent(event); mAttachInfo.mUnbufferedDispatchRequested = false; mAttachInfo.mHandlingPointerEvent = true; boolean handled = mView.dispatchPointerEvent(event); // If the event was fully handled by the handwriting initiator, then don't dispatch it // to the view tree. handled = handled || mView.dispatchPointerEvent(event); maybeUpdatePointerIcon(event); maybeUpdateTooltip(event); mAttachInfo.mHandlingPointerEvent = false; Loading core/tests/coretests/AndroidManifest.xml +11 −0 Original line number Diff line number Diff line Loading @@ -1680,6 +1680,17 @@ android:resizeableActivity="true" android:exported="true"> </activity> <service android:name="android.view.stylus.HandwritingImeService" android:label="Handwriting IME" android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true"> <intent-filter> <action android:name="android.view.InputMethod"/> </intent-filter> <meta-data android:name="android.view.im" android:resource="@xml/ime_meta_handwriting"/> </service> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" Loading core/tests/coretests/res/xml/ime_meta_handwriting.xml 0 → 100644 +21 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (C) 2022 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. --> <input-method xmlns:android="http://schemas.android.com/apk/res/android" android:settingsActivity="com.android.inputmethod.latin.settings.SettingsActivity" android:supportsStylusHandwriting="true"/> core/tests/coretests/src/android/view/stylus/HandwritingImeService.java 0 → 100644 +32 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view.stylus; import android.content.ComponentName; import android.inputmethodservice.InputMethodService; public class HandwritingImeService extends InputMethodService { private static final String PACKAGE_NAME = "com.android.frameworks.coretests"; private static ComponentName getComponentName() { return new ComponentName(PACKAGE_NAME, HandwritingImeService.class.getName()); } static String getImeId() { return getComponentName().flattenToShortString(); } } Loading
core/java/android/view/HandwritingInitiator.java +61 −41 Original line number Diff line number Diff line Loading @@ -60,7 +60,7 @@ public class HandwritingInitiator { */ private final long mHandwritingTimeoutInMillis; private final State mState = new State(); private State mState; private final HandwritingAreaTracker mHandwritingAreasTracker = new HandwritingAreaTracker(); /** The reference to the View that currently has the input connection. */ Loading @@ -86,17 +86,27 @@ public class HandwritingInitiator { /** * Notify the HandwritingInitiator that a new MotionEvent has arrived. * This method is non-block, and the event passed to this method should be dispatched to the * View tree as usual. If HandwritingInitiator triggers the handwriting mode, an fabricated * ACTION_CANCEL event will be sent to the ViewRootImpl. * @param motionEvent the stylus MotionEvent. * * <p>The return value indicates whether the event has been fully handled by the * HandwritingInitiator and should not be dispatched to the view tree. This will be true for * ACTION_MOVE events from a stylus gesture after handwriting mode has been initiated, in order * to suppress other actions such as scrolling. * * <p>If HandwritingInitiator triggers the handwriting mode, a fabricated ACTION_CANCEL event * will be sent to the ViewRootImpl. * * @param motionEvent the stylus {@link MotionEvent} * @return true if the event has been fully handled by the {@link HandwritingInitiator} and * should not be dispatched to the {@link View} tree, or false if the event should be dispatched * to the {@link View} tree as usual */ @VisibleForTesting public void onTouchEvent(@NonNull MotionEvent motionEvent) { public boolean onTouchEvent(@NonNull MotionEvent motionEvent) { final int maskedAction = motionEvent.getActionMasked(); switch (maskedAction) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: mState = null; final int actionIndex = motionEvent.getActionIndex(); final int toolType = motionEvent.getToolType(actionIndex); // TOOL_TYPE_ERASER is also from stylus. This indicates that the user is holding Loading @@ -104,42 +114,44 @@ public class HandwritingInitiator { if (toolType != MotionEvent.TOOL_TYPE_STYLUS && toolType != MotionEvent.TOOL_TYPE_ERASER) { // The motion event is not from a stylus event, ignore it. return; return false; } if (!mImm.isStylusHandwritingAvailable()) { return false; } mState.mStylusPointerId = motionEvent.getPointerId(actionIndex); mState.mStylusDownTimeInMillis = motionEvent.getEventTime(); mState.mStylusDownX = motionEvent.getX(actionIndex); mState.mStylusDownY = motionEvent.getY(actionIndex); mState.mStylusDownCandidateView = new WeakReference<>( findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY)); mState.mShouldInitHandwriting = true; mState.mExceedHandwritingSlop = false; mState = new State(motionEvent); break; case MotionEvent.ACTION_POINTER_UP: final int pointerId = motionEvent.getPointerId(motionEvent.getActionIndex()); if (pointerId != mState.mStylusPointerId) { if (mState == null || pointerId != mState.mStylusPointerId) { // ACTION_POINTER_UP is from another stylus pointer, ignore the event. return; return false; } // Deliberately fall through. case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // If it's ACTION_CANCEL or ACTION_UP, all the pointers go up. There is no need to // check whether the stylus we are tracking goes up. if (mState != null) { mState.mShouldInitHandwriting = false; break; } return false; case MotionEvent.ACTION_MOVE: if (mState == null) { return false; } // Either we've already tried to initiate handwriting, or the ongoing MotionEvent // sequence is considered to be tap, long-click or other gestures. if (!mState.mShouldInitHandwriting || mState.mExceedHandwritingSlop) { return; return mState.mHasInitiatedHandwriting; } final long timeElapsed = motionEvent.getEventTime() - mState.mStylusDownTimeInMillis; if (timeElapsed > mHandwritingTimeoutInMillis) { mState.mShouldInitHandwriting = false; return; return mState.mHasInitiatedHandwriting; } final int pointerIndex = motionEvent.findPointerIndex(mState.mStylusPointerId); Loading @@ -147,13 +159,8 @@ public class HandwritingInitiator { final float y = motionEvent.getY(pointerIndex); if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) { mState.mExceedHandwritingSlop = true; View candidateView = mState.mStylusDownCandidateView.get(); if (candidateView == null || !candidateView.isAttachedToWindow()) { // If there was no candidate view found in the stylus down event, or if that // candidate view is no longer attached, search again for a candidate view. candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); } View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); if (candidateView != null) { if (candidateView == getConnectedView()) { startHandwriting(candidateView); Loading @@ -162,7 +169,9 @@ public class HandwritingInitiator { } } } return mState.mHasInitiatedHandwriting; } return false; } @Nullable Loading Loading @@ -195,7 +204,7 @@ public class HandwritingInitiator { } else { mConnectedView = new WeakReference<>(view); mConnectionCount = 1; if (mState.mShouldInitHandwriting) { if (mState != null && mState.mShouldInitHandwriting) { tryStartHandwriting(); } } Loading Loading @@ -259,6 +268,7 @@ public class HandwritingInitiator { @VisibleForTesting public void startHandwriting(@NonNull View view) { mImm.startStylusHandwriting(view); mState.mHasInitiatedHandwriting = true; mState.mShouldInitHandwriting = false; } Loading Loading @@ -438,28 +448,38 @@ public class HandwritingInitiator { * b) If the MotionEvent sequence is considered to be tap, long-click or other gestures. * This boolean will be set to false, and it won't request to start handwriting. */ private boolean mShouldInitHandwriting = false; private boolean mShouldInitHandwriting; /** * Whether handwriting mode has already been initiated for the current MotionEvent sequence. */ private boolean mHasInitiatedHandwriting; /** * Whether the current ongoing stylus MotionEvent sequence already exceeds the * handwriting slop. * It's used for the case where the stylus exceeds handwriting slop before the target View * built InputConnection. */ private boolean mExceedHandwritingSlop = false; private boolean mExceedHandwritingSlop; /** The pointer id of the stylus pointer that is being tracked. */ private int mStylusPointerId = -1; private final int mStylusPointerId; /** The time stamp when the stylus pointer goes down. */ private long mStylusDownTimeInMillis = -1; private final long mStylusDownTimeInMillis; /** The initial location where the stylus pointer goes down. */ private float mStylusDownX = Float.NaN; private float mStylusDownY = Float.NaN; /** * The best candidate view to initialize handwriting mode based on the initial location * where the stylus pointer goes down, or null if the location was not within any candidate * view's handwriting area. */ private WeakReference<View> mStylusDownCandidateView = new WeakReference<>(null); private final float mStylusDownX; private final float mStylusDownY; private State(MotionEvent motionEvent) { final int actionIndex = motionEvent.getActionIndex(); mStylusPointerId = motionEvent.getPointerId(actionIndex); mStylusDownTimeInMillis = motionEvent.getEventTime(); mStylusDownX = motionEvent.getX(actionIndex); mStylusDownY = motionEvent.getY(actionIndex); mShouldInitHandwriting = true; mHasInitiatedHandwriting = false; mExceedHandwritingSlop = false; } } /** The helper method to check if the given view is still active for handwriting. */ Loading
core/java/android/view/ViewRootImpl.java +4 −2 Original line number Diff line number Diff line Loading @@ -6554,11 +6554,13 @@ public final class ViewRootImpl implements ViewParent, private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; mHandwritingInitiator.onTouchEvent(event); boolean handled = mHandwritingInitiator.onTouchEvent(event); mAttachInfo.mUnbufferedDispatchRequested = false; mAttachInfo.mHandlingPointerEvent = true; boolean handled = mView.dispatchPointerEvent(event); // If the event was fully handled by the handwriting initiator, then don't dispatch it // to the view tree. handled = handled || mView.dispatchPointerEvent(event); maybeUpdatePointerIcon(event); maybeUpdateTooltip(event); mAttachInfo.mHandlingPointerEvent = false; Loading
core/tests/coretests/AndroidManifest.xml +11 −0 Original line number Diff line number Diff line Loading @@ -1680,6 +1680,17 @@ android:resizeableActivity="true" android:exported="true"> </activity> <service android:name="android.view.stylus.HandwritingImeService" android:label="Handwriting IME" android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true"> <intent-filter> <action android:name="android.view.InputMethod"/> </intent-filter> <meta-data android:name="android.view.im" android:resource="@xml/ime_meta_handwriting"/> </service> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" Loading
core/tests/coretests/res/xml/ime_meta_handwriting.xml 0 → 100644 +21 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (C) 2022 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. --> <input-method xmlns:android="http://schemas.android.com/apk/res/android" android:settingsActivity="com.android.inputmethod.latin.settings.SettingsActivity" android:supportsStylusHandwriting="true"/>
core/tests/coretests/src/android/view/stylus/HandwritingImeService.java 0 → 100644 +32 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view.stylus; import android.content.ComponentName; import android.inputmethodservice.InputMethodService; public class HandwritingImeService extends InputMethodService { private static final String PACKAGE_NAME = "com.android.frameworks.coretests"; private static ComponentName getComponentName() { return new ComponentName(PACKAGE_NAME, HandwritingImeService.class.getName()); } static String getImeId() { return getComponentName().flattenToShortString(); } }