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:
@@ -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) {
|
||||
|
||||
BIN
core/res/res/drawable-hdpi/pointer_handwriting.png
Normal file
BIN
core/res/res/drawable-hdpi/pointer_handwriting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
core/res/res/drawable-mdpi/pointer_handwriting.png
Normal file
BIN
core/res/res/drawable-mdpi/pointer_handwriting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 912 B |
BIN
core/res/res/drawable-xhdpi/pointer_handwriting.png
Normal file
BIN
core/res/res/drawable-xhdpi/pointer_handwriting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
core/res/res/drawable-xxhdpi/pointer_handwriting.png
Normal file
BIN
core/res/res/drawable-xxhdpi/pointer_handwriting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
@@ -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" />
|
||||
@@ -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 */);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user