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
This commit is contained in:
Haoyu Zhang
2023-04-26 11:05:26 -07:00
parent ac4c6aa72f
commit fa242f4a93
7 changed files with 207 additions and 17 deletions

View File

@@ -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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -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" />
android:bitmap="@drawable/pointer_handwriting"
android:hotSpotX="8.25dp"
android:hotSpotY="23.75dp" />

View File

@@ -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 */);
}
}