diff options
| -rw-r--r-- | core/java/android/view/HandwritingInitiator.java | 77 | ||||
| -rw-r--r-- | core/res/res/drawable-hdpi/pointer_handwriting.png | bin | 0 -> 1609 bytes | |||
| -rw-r--r-- | core/res/res/drawable-mdpi/pointer_handwriting.png | bin | 0 -> 912 bytes | |||
| -rw-r--r-- | core/res/res/drawable-xhdpi/pointer_handwriting.png | bin | 0 -> 2504 bytes | |||
| -rw-r--r-- | core/res/res/drawable-xxhdpi/pointer_handwriting.png | bin | 0 -> 4983 bytes | |||
| -rw-r--r-- | core/res/res/drawable/pointer_handwriting_icon.xml | 6 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java | 141 |
7 files changed, 207 insertions, 17 deletions
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index dd4f9644da96..ab58306ba5ab 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -85,7 +85,21 @@ public class HandwritingInitiator { * to {@link #findBestCandidateView(float, float)}. */ @Nullable - private View mCachedHoverTarget = null; + private WeakReference<View> mCachedHoverTarget = null; + + /** + * Whether to show the hover icon for the current connected view. + * Hover icon should be hidden for the current connected view after handwriting is initiated + * for it until one of the following events happens: + * a) user performs a click or long click. In other words, if it receives a series of motion + * events that don't trigger handwriting, show hover icon again. + * b) the stylus hovers on another editor that supports handwriting (or a handwriting delegate). + * c) the current connected editor lost focus. + * + * If the stylus is hovering on an unconnected editor that supports handwriting, we always show + * the hover icon. + */ + private boolean mShowHoverIconForConnectedView = true; @VisibleForTesting public HandwritingInitiator(@NonNull ViewConfiguration viewConfiguration, @@ -142,6 +156,12 @@ public class HandwritingInitiator { // check whether the stylus we are tracking goes up. if (mState != null) { mState.mShouldInitHandwriting = false; + if (!mState.mHasInitiatedHandwriting + && !mState.mHasPreparedHandwritingDelegation) { + // The user just did a click, long click or another stylus gesture, + // show hover icon again for the connected view. + mShowHoverIconForConnectedView = true; + } } return false; case MotionEvent.ACTION_MOVE: @@ -214,7 +234,11 @@ public class HandwritingInitiator { */ public void onDelegateViewFocused(@NonNull View view) { if (view == getConnectedView()) { - tryAcceptStylusHandwritingDelegation(view); + if (tryAcceptStylusHandwritingDelegation(view)) { + // A handwriting delegate view is accepted and handwriting starts; hide the + // hover icon. + mShowHoverIconForConnectedView = false; + } } } @@ -237,7 +261,12 @@ public class HandwritingInitiator { } else { mConnectedView = new WeakReference<>(view); mConnectionCount = 1; + // A new view just gain focus. By default, we should show hover icon for it. + mShowHoverIconForConnectedView = true; if (view.isHandwritingDelegate() && tryAcceptStylusHandwritingDelegation(view)) { + // A handwriting delegate view is accepted and handwriting starts; hide the + // hover icon. + mShowHoverIconForConnectedView = false; return; } if (mState != null && mState.mShouldInitHandwriting) { @@ -306,6 +335,7 @@ public class HandwritingInitiator { mImm.startStylusHandwriting(view); mState.mHasInitiatedHandwriting = true; mState.mShouldInitHandwriting = false; + mShowHoverIconForConnectedView = false; if (view instanceof TextView) { ((TextView) view).hideHint(); } @@ -361,15 +391,35 @@ public class HandwritingInitiator { * handwrite-able area. */ public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) { - if (shouldShowHandwritingPointerIcon(event)) { + final View hoverView = findHoverView(event); + if (hoverView == null) { + return null; + } + + if (mShowHoverIconForConnectedView) { + return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING); + } + + if (hoverView != getConnectedView()) { + // The stylus is hovering on another view that supports handwriting. We should show + // hover icon. Also reset the mShowHoverIconForConnectedView so that hover + // icon is displayed again next time when the stylus hovers on connected view. + mShowHoverIconForConnectedView = true; return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING); } return null; } - private boolean shouldShowHandwritingPointerIcon(MotionEvent event) { + private View getCachedHoverTarget() { + if (mCachedHoverTarget == null) { + return null; + } + return mCachedHoverTarget.get(); + } + + private View findHoverView(MotionEvent event) { if (!event.isStylusPointer() || !event.isHoverEvent()) { - return false; + return null; } if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER @@ -377,24 +427,25 @@ public class HandwritingInitiator { final float hoverX = event.getX(event.getActionIndex()); final float hoverY = event.getY(event.getActionIndex()); - if (mCachedHoverTarget != null) { - final Rect handwritingArea = getViewHandwritingArea(mCachedHoverTarget); - if (isInHandwritingArea(handwritingArea, hoverX, hoverY, mCachedHoverTarget) - && shouldTriggerStylusHandwritingForView(mCachedHoverTarget)) { - return true; + final View cachedHoverTarget = getCachedHoverTarget(); + if (cachedHoverTarget != null) { + final Rect handwritingArea = getViewHandwritingArea(cachedHoverTarget); + if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget) + && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) { + return cachedHoverTarget; } } final View candidateView = findBestCandidateView(hoverX, hoverY); if (candidateView != null) { - mCachedHoverTarget = candidateView; - return true; + mCachedHoverTarget = new WeakReference<>(candidateView); + return candidateView; } } mCachedHoverTarget = null; - return false; + return null; } private static void requestFocusWithoutReveal(View view) { diff --git a/core/res/res/drawable-hdpi/pointer_handwriting.png b/core/res/res/drawable-hdpi/pointer_handwriting.png Binary files differnew file mode 100644 index 000000000000..6d7c59cccfc7 --- /dev/null +++ b/core/res/res/drawable-hdpi/pointer_handwriting.png diff --git a/core/res/res/drawable-mdpi/pointer_handwriting.png b/core/res/res/drawable-mdpi/pointer_handwriting.png Binary files differnew file mode 100644 index 000000000000..b36241bec84e --- /dev/null +++ b/core/res/res/drawable-mdpi/pointer_handwriting.png diff --git a/core/res/res/drawable-xhdpi/pointer_handwriting.png b/core/res/res/drawable-xhdpi/pointer_handwriting.png Binary files differnew file mode 100644 index 000000000000..dea1972a6216 --- /dev/null +++ b/core/res/res/drawable-xhdpi/pointer_handwriting.png diff --git a/core/res/res/drawable-xxhdpi/pointer_handwriting.png b/core/res/res/drawable-xxhdpi/pointer_handwriting.png Binary files differnew file mode 100644 index 000000000000..870c40206e3c --- /dev/null +++ b/core/res/res/drawable-xxhdpi/pointer_handwriting.png diff --git a/core/res/res/drawable/pointer_handwriting_icon.xml b/core/res/res/drawable/pointer_handwriting_icon.xml index cdbf6938bd57..bba4e6e4c2e6 100644 --- a/core/res/res/drawable/pointer_handwriting_icon.xml +++ b/core/res/res/drawable/pointer_handwriting_icon.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <pointer-icon xmlns:android="http://schemas.android.com/apk/res/android" - android:bitmap="@drawable/pointer_crosshair" - android:hotSpotX="12dp" - android:hotSpotY="12dp" />
\ No newline at end of file + android:bitmap="@drawable/pointer_handwriting" + android:hotSpotX="8.25dp" + android:hotSpotY="23.75dp" />
\ No newline at end of file diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index c0125afef2e8..34eac35d3c0b 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -17,6 +17,7 @@ package android.view.stylus; import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.stylus.HandwritingTestUtil.createView; @@ -26,6 +27,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -42,6 +44,7 @@ import android.platform.test.annotations.Presubmit; import android.view.HandwritingInitiator; import android.view.InputDevice; import android.view.MotionEvent; +import android.view.PointerIcon; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; @@ -115,6 +118,7 @@ public class HandwritingInitiatorTest { HW_BOUNDS_OFFSETS_BOTTOM_PX); mHandwritingInitiator.updateHandwritingAreasForView(mTestView1); mHandwritingInitiator.updateHandwritingAreasForView(mTestView2); + doReturn(true).when(mHandwritingInitiator).tryAcceptStylusHandwritingDelegation(any()); } @Test @@ -486,6 +490,112 @@ public class HandwritingInitiatorTest { } @Test + public void onResolvePointerIcon_withinHWArea_showPointerIcon() { + MotionEvent hoverEvent = createStylusHoverEvent(sHwArea1.centerX(), sHwArea1.centerY()); + PointerIcon icon = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent); + assertThat(icon.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING); + } + + @Test + public void onResolvePointerIcon_withinExtendedHWArea_showPointerIcon() { + int x = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2; + int y = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2; + MotionEvent hoverEvent = createStylusHoverEvent(x, y); + + PointerIcon icon = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent); + assertThat(icon.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING); + } + + @Test + public void onResolvePointerIcon_afterHandwriting_hidePointerIconForConnectedView() { + // simulate the case where sTestView1 is focused. + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), + /* exceedsHWSlop */ true); + // Verify that handwriting started for sTestView1. + verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); + + MotionEvent hoverEvent1 = createStylusHoverEvent(sHwArea1.centerX(), sHwArea1.centerY()); + PointerIcon icon1 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1); + // After handwriting is initiated for the connected view, hide the hover icon. + assertThat(icon1).isNull(); + + MotionEvent hoverEvent2 = createStylusHoverEvent(sHwArea2.centerX(), sHwArea2.centerY()); + PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent2); + // Now stylus is hovering on another editor, show the hover icon. + assertThat(icon2.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING); + + // After the hover icon is displayed again, it will show hover icon for the connected view + // again. + PointerIcon icon3 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1); + assertThat(icon3.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING); + } + + @Test + public void onResolvePointerIcon_afterHandwriting_hidePointerIconForDelegatorView() { + // Set mTextView2 to be the delegate of mTestView1. + mTestView2.setIsHandwritingDelegate(true); + + mTestView1.setHandwritingDelegatorCallback( + () -> mHandwritingInitiator.onInputConnectionCreated(mTestView2)); + + injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), + /* exceedsHWSlop */ true); + // Prerequisite check, verify that handwriting started for delegateView. + verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(mTestView2); + + MotionEvent hoverEvent = createStylusHoverEvent(sHwArea2.centerX(), sHwArea2.centerY()); + PointerIcon icon = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent); + // After handwriting is initiated for the connected view, hide the hover icon. + assertThat(icon).isNull(); + } + + @Test + public void onResolvePointerIcon_showHoverIconAfterTap() { + // Simulate the case where sTestView1 is focused. + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), + /* exceedsHWSlop */ true); + // Verify that handwriting started for sTestView1. + verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); + + MotionEvent hoverEvent1 = createStylusHoverEvent(sHwArea1.centerX(), sHwArea1.centerY()); + PointerIcon icon1 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1); + // After handwriting is initiated for the connected view, hide the hover icon. + assertThat(icon1).isNull(); + + // When exceedsHwSlop is false, it simulates a tap. + injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), + /* exceedsHWSlop */ false); + + PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1); + assertThat(icon2.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING); + } + + @Test + public void onResolvePointerIcon_showHoverIconAfterFocusChange() { + // Simulate the case where sTestView1 is focused. + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), + /* exceedsHWSlop */ true); + // Verify that handwriting started for sTestView1. + verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); + + MotionEvent hoverEvent1 = createStylusHoverEvent(sHwArea1.centerX(), sHwArea1.centerY()); + PointerIcon icon1 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1); + // After handwriting is initiated for the connected view, hide the hover icon. + assertThat(icon1).isNull(); + + // Simulate that focus is switched to mTestView2 first and then switched back. + mHandwritingInitiator.onInputConnectionCreated(mTestView2); + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + + PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1); + // After the change of focus, hover icon shows again. + assertThat(icon2.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING); + } + + @Test public void autoHandwriting_whenDisabled_wontStartHW() { View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */, true /* isStylusHandwritingAvailable */); @@ -657,6 +767,35 @@ public class HandwritingInitiatorTest { return canvas; } + /** + * Inject {@link MotionEvent}s to the {@link HandwritingInitiator}. + * @param x the x coordinate of the first {@link MotionEvent}. + * @param y the y coordinate of the first {@link MotionEvent}. + * @param exceedsHWSlop whether the injected {@link MotionEvent} movements exceed the + * handwriting slop. If true, it simulates handwriting. Otherwise, it + * simulates a tap/click, + */ + private void injectStylusEvent(HandwritingInitiator handwritingInitiator, int x, int y, + boolean exceedsHWSlop) { + MotionEvent event1 = createStylusEvent(ACTION_DOWN, x, y, 0); + + if (exceedsHWSlop) { + x += mHandwritingSlop * 2; + } else { + x += mHandwritingSlop / 2; + } + MotionEvent event2 = createStylusEvent(ACTION_MOVE, x, y, 0); + MotionEvent event3 = createStylusEvent(ACTION_UP, x, y, 0); + + handwritingInitiator.onTouchEvent(event1); + handwritingInitiator.onTouchEvent(event2); + handwritingInitiator.onTouchEvent(event3); + } + + private MotionEvent createStylusHoverEvent(int x, int y) { + return createStylusEvent(ACTION_HOVER_MOVE, x, y, /* eventTime */ 0); + } + private MotionEvent createStylusEvent(int action, int x, int y, long eventTime) { MotionEvent.PointerProperties[] properties = MotionEvent.PointerProperties.createArray(1); properties[0].toolType = MotionEvent.TOOL_TYPE_STYLUS; @@ -668,6 +807,6 @@ public class HandwritingInitiatorTest { return MotionEvent.obtain(0 /* downTime */, eventTime /* eventTime */, action, 1, properties, coords, 0 /* metaState */, 0 /* buttonState */, 1 /* xPrecision */, 1 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */, - InputDevice.SOURCE_TOUCHSCREEN, 0 /* flags */); + InputDevice.SOURCE_STYLUS, 0 /* flags */); } } |