summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/view/HandwritingInitiator.java77
-rw-r--r--core/res/res/drawable-hdpi/pointer_handwriting.pngbin0 -> 1609 bytes
-rw-r--r--core/res/res/drawable-mdpi/pointer_handwriting.pngbin0 -> 912 bytes
-rw-r--r--core/res/res/drawable-xhdpi/pointer_handwriting.pngbin0 -> 2504 bytes
-rw-r--r--core/res/res/drawable-xxhdpi/pointer_handwriting.pngbin0 -> 4983 bytes
-rw-r--r--core/res/res/drawable/pointer_handwriting_icon.xml6
-rw-r--r--core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java141
7 files changed, 207 insertions, 17 deletions
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index dd4f9644da96..ab58306ba5ab 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<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) {
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 000000000000..6d7c59cccfc7
--- /dev/null
+++ b/core/res/res/drawable-hdpi/pointer_handwriting.png
Binary files differ
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 000000000000..b36241bec84e
--- /dev/null
+++ b/core/res/res/drawable-mdpi/pointer_handwriting.png
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/pointer_handwriting.png b/core/res/res/drawable-xhdpi/pointer_handwriting.png
new file mode 100644
index 000000000000..dea1972a6216
--- /dev/null
+++ b/core/res/res/drawable-xhdpi/pointer_handwriting.png
Binary files differ
diff --git a/core/res/res/drawable-xxhdpi/pointer_handwriting.png b/core/res/res/drawable-xxhdpi/pointer_handwriting.png
new file mode 100644
index 000000000000..870c40206e3c
--- /dev/null
+++ b/core/res/res/drawable-xxhdpi/pointer_handwriting.png
Binary files differ
diff --git a/core/res/res/drawable/pointer_handwriting_icon.xml b/core/res/res/drawable/pointer_handwriting_icon.xml
index cdbf6938bd57..bba4e6e4c2e6 100644
--- a/core/res/res/drawable/pointer_handwriting_icon.xml
+++ b/core/res/res/drawable/pointer_handwriting_icon.xml
@@ -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" /> \ 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 c0125afef2e8..34eac35d3c0b 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
@@ -486,6 +490,112 @@ public class HandwritingInitiatorTest {
}
@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 */,
true /* isStylusHandwritingAvailable */);
@@ -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 */);
}
}