Merge "Introduce handwriting hover icon and update its hide/show logic" into udc-dev

This commit is contained in:
Haoyu Zhang
2023-05-09 22:32:28 +00:00
committed by Android (Google) Code Review
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 */);
}
}