From fa242f4a935363cc490b3d0619788ad372654942 Mon Sep 17 00:00:00 2001 From: Haoyu Zhang Date: Wed, 26 Apr 2023 11:05:26 -0700 Subject: [PATCH] Introduce handwriting hover icon and update its hide/show logic Updated the hover icon for PointerIcon.TYPE_HANDWRITING. And also updated the hide/show logic of the handwriting hover icon. By default handwriting hover icon is displayed for areas where handwriting is available. Now the hover icon will hide after handwriting is triggered on the active editor until one of the following events happens: a) stylus clicks something. b) stylus hovers on another editor that supports handwriting. c) the current focused editor lost focus. The hotspot of the pointer icon is designed to be x: 34% of the width; 8.25 dp (rouneded to the closest quarter-dp). y: 100% of height, which is set to 23.75dp because hotSpotY must be less than bitmap height. Bug: 274696514 Test: atest HandwritingInitiatorTest Change-Id: I9270076fc3f5eae78386fb5c2e15f9872ddf0c40 --- .../android/view/HandwritingInitiator.java | 77 ++++++++-- .../res/drawable-hdpi/pointer_handwriting.png | Bin 0 -> 1609 bytes .../res/drawable-mdpi/pointer_handwriting.png | Bin 0 -> 912 bytes .../drawable-xhdpi/pointer_handwriting.png | Bin 0 -> 2504 bytes .../drawable-xxhdpi/pointer_handwriting.png | Bin 0 -> 4983 bytes .../res/drawable/pointer_handwriting_icon.xml | 6 +- .../view/stylus/HandwritingInitiatorTest.java | 141 +++++++++++++++++- 7 files changed, 207 insertions(+), 17 deletions(-) create mode 100644 core/res/res/drawable-hdpi/pointer_handwriting.png create mode 100644 core/res/res/drawable-mdpi/pointer_handwriting.png create mode 100644 core/res/res/drawable-xhdpi/pointer_handwriting.png create mode 100644 core/res/res/drawable-xxhdpi/pointer_handwriting.png diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index dd4f9644da964..ab58306ba5ab5 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 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 new file mode 100644 index 0000000000000000000000000000000000000000..6d7c59cccfc721e6dd5836f544834292d9aebb5b GIT binary patch literal 1609 zcmV-P2DbT$P)X=nloiQPyH34#Mbh+4l&7X}ie6F*9dVqu&&r;E9Fc=Py>0T;c=$-DF3 zyZ8Oh|D5|ZhKTTgDh^#S48s5n%QI_Tma5AN-l+Z-m6h567$Tw@Co6~~Wg1YFMJ%T8 z#eKjAu@q1o%u;Y#Aq|qO?j>6Q1u&I`)inmT41`^6JCG;`7J*rT96$=-1f2c-{Z%e|=d-d>rNm6dLT(f#P0#j21C zcpE4l8yh<(BI0tn#4rqTyWJupGB!4L4k%XdX_Z_SRW<`&AkXLXl}=7h{t}5q=C#V9 zQ0QqO5cmoxRDEbbmx4$H8Fms%e^1Utb^BM@mYHbaZrxh{&BgciI3yuubLK zMj%;476I)X9v;516wvVS@C9HO3(1|Xz_tQDzu&(*7z~a^A`v-!_;6f%Z4QS+&YwRo zA|ltWU26u4EGgiy1LXlb1A##2Qb2(~pcD8=b+Q%6266zuhA~Z3>gwv^+Sb(6h|}qe zgX-(+6Vo*34jed81>`HoQW65XdiCnBK!r-|JY~f`pRZy$nELv9F-=o?dV1oZ%FD|| zMC9`2%Z;kPY{0n|pi1C975|c=qM}chgE@5QkVGPp*j(1s)D(M`mX@Yf{~35+Dekh4 zve%YKB%)_?Cy=sz`*zRJ&`_(_>)mzm;6d8k+hgz6)z#6~))reDZr;4f*|TS3&nqh{ z0l0tvevtTOYV`_TxpJio*b7t?6cp@z^ytypN@3FF#*G`|^?GB^Gcqz{Vq!vOXJ@B< zKHq-eLm(G$Cls=Qfq~zEn%vynFCIR8IKI?qdL2x9db*5`j!HNj4xKu6>T6)1J!Go} z>hJIG_IkZvPEAdXtp~<5%~xm6ocSK8RXO?Ynv!~8VBq(usi{BKtIX-sr@saE1D^pO zsPAq`c-V)-;gE=|DQCvV$0ZyNhnkz4HOyXM2hlk#o3%`gy4Yc?Lwlt#wY0RfD42aj zmmt5&oiw70^P;6AVW2&G_Q=f4OrkL5DsQ$CU4AwaU6_;B4Wyw42M5;*qqiRp)CO(c zKz3ULOl-p^mL208DQRhGT)K3L^78Uy2VMYkEbPmzCmDb@2b2Iz zh{dOu*vVtnYnd*6eSJ&bFDfb`KRVknFjt=_DtDRWzW=ot&WD#oA?!BVBu`wbjJ&}f$O7p~@a^ptRaxhjP7ty(4vjR(R zY;3G3D=RC!b?esTi4!MAmDm3e4a2*wJ|<7%NZ;(FJa!g3@(|>hPsf z=Q+_Z=mfO`=?_~W^jZG3FtCu+LR8*4qInfj^<+zZR$b>5%yK~3?ynlg4iqLD3JvE} zK&JX_{r4m8FBw~(ad4{zSq@0ctktsrKV&t#I`LEqg6Q%e0<%Bi!y<(d00000NkvXX Hu0mjf7y;#{ literal 0 HcmV?d00001 diff --git a/core/res/res/drawable-mdpi/pointer_handwriting.png b/core/res/res/drawable-mdpi/pointer_handwriting.png new file mode 100644 index 0000000000000000000000000000000000000000..b36241bec84ec8e1edd5f40be1914e9fbd1b5642 GIT binary patch literal 912 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 \ 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 c0125afef2e8f..34eac35d3c0b5 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 @@ -485,6 +489,112 @@ public class HandwritingInitiatorTest { verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); } + @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 */, @@ -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 */); } }