Merge "Introduce handwriting hover icon and update its hide/show logic" into udc-dev
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