Merge "Scribe in View: Introduce HandwritingInitiator"
This commit is contained in:
303
core/java/android/view/HandwritingInitiator.java
Normal file
303
core/java/android/view/HandwritingInitiator.java
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.view;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.graphics.Rect;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Initiates handwriting mode once it detects stylus movement in handwritable areas.
|
||||
*
|
||||
* It is designed to be used by {@link ViewRootImpl}. For every stylus related MotionEvent that is
|
||||
* dispatched to view tree, ViewRootImpl should call {@link #onTouchEvent} method of this class.
|
||||
* And it will automatically request to enter the handwriting mode when the conditions meet.
|
||||
*
|
||||
* Notice that ViewRootImpl should still dispatch MotionEvents to view tree as usual.
|
||||
* And if it successfully enters the handwriting mode, the ongoing MotionEvent stream will be
|
||||
* routed to the input method. Input system will fabricate an ACTION_CANCEL and send to
|
||||
* ViewRootImpl.
|
||||
*
|
||||
* This class does nothing if:
|
||||
* a) MotionEvents are not from stylus.
|
||||
* b) The user taps or long-clicks with a stylus etc.
|
||||
* c) Stylus pointer down position is not within a handwritable area.
|
||||
*
|
||||
* Used by InputMethodManager.
|
||||
* @hide
|
||||
*/
|
||||
public class HandwritingInitiator {
|
||||
/**
|
||||
* The touchSlop from {@link ViewConfiguration} used to decide whether a pointer is considered
|
||||
* moving or stationary.
|
||||
*/
|
||||
private final int mTouchSlop;
|
||||
/**
|
||||
* The timeout used to distinguish tap from handwriting. If the stylus doesn't move before this
|
||||
* timeout, it's not considered as handwriting.
|
||||
*/
|
||||
private final long mTapTimeoutInMillis;
|
||||
|
||||
private State mState = new State();
|
||||
|
||||
/**
|
||||
* Helper method to reset the internal state of this class.
|
||||
* Calling this method will also prevent the following MotionEvents
|
||||
* triggers handwriting until the next stylus ACTION_DOWN/ACTION_POINTER_DOWN
|
||||
* arrives.
|
||||
*/
|
||||
private void reset() {
|
||||
mState = new State();
|
||||
}
|
||||
|
||||
/** The reference to the View that currently has the input connection. */
|
||||
@Nullable
|
||||
@VisibleForTesting
|
||||
public WeakReference<View> mConnectedView = null;
|
||||
|
||||
/** The editor bound reported by the connected View. */
|
||||
@Nullable
|
||||
@VisibleForTesting
|
||||
public Rect mEditorBound = null;
|
||||
|
||||
/**
|
||||
* When InputConnection restarts for a View, View#onInputConnectionCreatedInternal
|
||||
* might be called before View#onInputConnectionClosedInternal, so we need to count the input
|
||||
* connections and only set mConnectedView to null when mConnectionCount is zero.
|
||||
*/
|
||||
private int mConnectionCount = 0;
|
||||
private final InputMethodManager mImm;
|
||||
|
||||
@VisibleForTesting
|
||||
public HandwritingInitiator(ViewConfiguration viewConfiguration,
|
||||
InputMethodManager inputMethodManager) {
|
||||
mTouchSlop = viewConfiguration.getScaledTouchSlop();
|
||||
mTapTimeoutInMillis = ViewConfiguration.getTapTimeout();
|
||||
mImm = inputMethodManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the HandwritingInitiator that a new MotionEvent has arrived.
|
||||
* This method is non-block, and the event passed to this method should be dispatched to the
|
||||
* View tree as usual. If HandwritingInitiator triggers the handwriting mode, an fabricated
|
||||
* ACTION_CANCEL event will be sent to the ViewRootImpl.
|
||||
* @param motionEvent the stylus MotionEvent.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public void onTouchEvent(MotionEvent motionEvent) {
|
||||
final int maskedAction = motionEvent.getActionMasked();
|
||||
switch (maskedAction) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
final int actionIndex = motionEvent.getActionIndex();
|
||||
final int toolType = motionEvent.getToolType(actionIndex);
|
||||
// TOOL_TYPE_ERASER is also from stylus. This indicates that the user is holding
|
||||
// the eraser button during handwriting.
|
||||
if (toolType != MotionEvent.TOOL_TYPE_STYLUS
|
||||
&& toolType != MotionEvent.TOOL_TYPE_ERASER) {
|
||||
// The motion event is not from a stylus event, ignore it.
|
||||
return;
|
||||
}
|
||||
mState.mStylusPointerId = motionEvent.getPointerId(actionIndex);
|
||||
mState.mStylusDownTimeInMillis = motionEvent.getEventTime();
|
||||
mState.mStylusDownX = motionEvent.getX(actionIndex);
|
||||
mState.mStylusDownY = motionEvent.getY(actionIndex);
|
||||
mState.mShouldInitHandwriting = true;
|
||||
mState.mExceedTouchSlop = false;
|
||||
break;
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
final int pointerId = motionEvent.getPointerId(motionEvent.getActionIndex());
|
||||
if (pointerId != mState.mStylusPointerId) {
|
||||
// ACTION_POINTER_UP is from another stylus pointer, ignore the event.
|
||||
return;
|
||||
}
|
||||
// Deliberately fall through.
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
// If it's ACTION_CANCEL or ACTION_UP, all the pointers go up. There is no need to
|
||||
// check whether the stylus we are tracking goes up.
|
||||
reset();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// Either we've already tried to initiate handwriting, or the ongoing MotionEvent
|
||||
// sequence is considered to be tap, long-click or other gestures.
|
||||
if (!mState.mShouldInitHandwriting || mState.mExceedTouchSlop) {
|
||||
return;
|
||||
}
|
||||
|
||||
final long timeElapsed =
|
||||
motionEvent.getEventTime() - mState.mStylusDownTimeInMillis;
|
||||
if (timeElapsed > mTapTimeoutInMillis) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
final int pointerIndex = motionEvent.findPointerIndex(mState.mStylusPointerId);
|
||||
final float x = motionEvent.getX(pointerIndex);
|
||||
final float y = motionEvent.getY(pointerIndex);
|
||||
if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) {
|
||||
mState.mExceedTouchSlop = true;
|
||||
tryStartHandwriting();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private View getConnectedView() {
|
||||
if (mConnectedView == null) return null;
|
||||
return mConnectedView.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify HandwritingInitiator that a new InputConnection is created.
|
||||
* The caller of this method should guarantee that each onInputConnectionCreated call
|
||||
* is paired with a onInputConnectionClosed call.
|
||||
* @param view the view that created the current InputConnection.
|
||||
* @see #onInputConnectionClosed(View)
|
||||
*/
|
||||
public void onInputConnectionCreated(@NonNull View view, @NonNull EditorInfo editorInfo) {
|
||||
final View connectedView = getConnectedView();
|
||||
// updateEditorBound(editorInfo.getInitialEditorBound());
|
||||
if (connectedView == view) {
|
||||
++mConnectionCount;
|
||||
} else {
|
||||
mConnectedView = new WeakReference<>(view);
|
||||
mConnectionCount = 1;
|
||||
tryStartHandwriting();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify HandwritingInitiator that the InputConnection has closed for the given view.
|
||||
* The caller of this method should guarantee that each onInputConnectionClosed call
|
||||
* is paired with a onInputConnectionCreated call.
|
||||
* @param view the view that closed the InputConnection.
|
||||
*/
|
||||
public void onInputConnectionClosed(@NonNull View view) {
|
||||
final View connectedView = getConnectedView();
|
||||
if (connectedView == view) {
|
||||
--mConnectionCount;
|
||||
if (mConnectionCount == 0) {
|
||||
mConnectedView = null;
|
||||
mEditorBound = null;
|
||||
}
|
||||
} else {
|
||||
// Unexpected branch, set mConnectedView to null to avoid further problem.
|
||||
mConnectedView = null;
|
||||
mEditorBound = null;
|
||||
mConnectionCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the HandwritingInitiator that editor bound of the connected view(the view with
|
||||
* active InputConnection) has be updated.
|
||||
* @param editorBound new the editor bounds of the connected view.
|
||||
*/
|
||||
public void updateEditorBound(@NonNull Rect editorBound) {
|
||||
if (mEditorBound == null) {
|
||||
mEditorBound = new Rect(editorBound);
|
||||
} else {
|
||||
mEditorBound.left = editorBound.left;
|
||||
mEditorBound.top = editorBound.top;
|
||||
mEditorBound.right = editorBound.right;
|
||||
mEditorBound.bottom = editorBound.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to initiate handwriting. For this method to successfully send startHandwriting signal,
|
||||
* the following 3 conditions should meet:
|
||||
* a) The stylus movement exceeds the touchSlop.
|
||||
* b) A View has built InputConnection with IME.
|
||||
* c) The stylus event lands into the connected View's boundary.
|
||||
* This method will immediately fail without any side effect if condition a or b is not met.
|
||||
* However, if both condition a and b are met but the condition c is not met, it will reset the
|
||||
* internal states. And HandwritingInitiator won't attempt to call startHandwriting until the
|
||||
* next ACTION_DOWN.
|
||||
*/
|
||||
private void tryStartHandwriting() {
|
||||
if (!mState.mExceedTouchSlop) {
|
||||
return;
|
||||
}
|
||||
final View connectedView = getConnectedView();
|
||||
if (connectedView == null || mEditorBound == null) {
|
||||
return;
|
||||
}
|
||||
final ViewParent viewParent = connectedView.getParent();
|
||||
// Do a final check before startHandwriting.
|
||||
if (viewParent != null && connectedView.isAttachedToWindow()) {
|
||||
final Rect editorBounds = new Rect(mEditorBound);
|
||||
if (viewParent.getChildVisibleRect(connectedView, editorBounds, null)) {
|
||||
final int roundedInitX = Math.round(mState.mStylusDownX);
|
||||
final int roundedInitY = Math.round(mState.mStylusDownY);
|
||||
if (editorBounds.contains(roundedInitX, roundedInitY)) {
|
||||
startHandwriting(mConnectedView.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
reset();
|
||||
}
|
||||
|
||||
/** For test only. */
|
||||
@VisibleForTesting
|
||||
public void startHandwriting(View view) {
|
||||
// mImm.startHandwriting(view);
|
||||
}
|
||||
|
||||
private boolean largerThanTouchSlop(float x1, float y1, float x2, float y2) {
|
||||
float dx = x1 - x2;
|
||||
float dy = y1 - y2;
|
||||
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
|
||||
}
|
||||
|
||||
/** Object that keeps the MotionEvent related states for HandwritingInitiator. */
|
||||
private static class State {
|
||||
/**
|
||||
* Whether it should initiate handwriting mode for the current MotionEvent sequence.
|
||||
* (A series of MotionEvents from ACTION_DOWN to ACTION_UP)
|
||||
*
|
||||
* The purpose of this boolean value is:
|
||||
* a) We should only request to start handwriting mode ONCE for each MotionEvent sequence.
|
||||
* If we've already requested to enter handwriting mode for the ongoing MotionEvent
|
||||
* sequence, this boolean is set to false. And it won't request to start handwriting again.
|
||||
*
|
||||
* b) If the MotionEvent sequence is considered to be tap, long-click or other gestures.
|
||||
* This boolean will be set to false, and it won't request to start handwriting.
|
||||
*/
|
||||
private boolean mShouldInitHandwriting = false;
|
||||
/**
|
||||
* Whether the current ongoing stylus MotionEvent sequence already exceeds the touchSlop.
|
||||
* It's used for the case where the stylus exceeds touchSlop before the target View built
|
||||
* InputConnection.
|
||||
*/
|
||||
private boolean mExceedTouchSlop = false;
|
||||
|
||||
/** The pointer id of the stylus pointer that is being tracked. */
|
||||
private int mStylusPointerId = -1;
|
||||
/** The time stamp when the stylus pointer goes down. */
|
||||
private long mStylusDownTimeInMillis = -1;
|
||||
/** The initial location where the stylus pointer goes down. */
|
||||
private float mStylusDownX = Float.NaN;
|
||||
private float mStylusDownY = Float.NaN;
|
||||
}
|
||||
}
|
||||
@@ -752,6 +752,17 @@ public final class ViewRootImpl implements ViewParent,
|
||||
int localChanges;
|
||||
}
|
||||
|
||||
private final HandwritingInitiator mHandwritingInitiator;
|
||||
|
||||
/**
|
||||
* Used by InputMethodManager.
|
||||
* @hide
|
||||
*/
|
||||
@NonNull
|
||||
public HandwritingInitiator getHandwritingInitiator() {
|
||||
return mHandwritingInitiator;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only used on the RenderThread when handling a blast sync. Specifically, it's only
|
||||
* used when calling {@link BLASTBufferQueue#setSyncTransaction(Transaction)} and then merged
|
||||
@@ -826,6 +837,8 @@ public final class ViewRootImpl implements ViewParent,
|
||||
? Choreographer.getSfInstance() : Choreographer.getInstance();
|
||||
mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
|
||||
mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
|
||||
mHandwritingInitiator = new HandwritingInitiator(mViewConfiguration,
|
||||
mContext.getSystemService(InputMethodManager.class));
|
||||
|
||||
String processorOverrideName = context.getResources().getString(
|
||||
R.string.config_inputEventCompatProcessorOverrideClassName);
|
||||
@@ -6418,6 +6431,7 @@ public final class ViewRootImpl implements ViewParent,
|
||||
|
||||
private int processPointerEvent(QueuedInputEvent q) {
|
||||
final MotionEvent event = (MotionEvent)q.mEvent;
|
||||
mHandwritingInitiator.onTouchEvent(event);
|
||||
|
||||
mAttachInfo.mUnbufferedDispatchRequested = false;
|
||||
mAttachInfo.mHandlingPointerEvent = true;
|
||||
|
||||
@@ -2093,6 +2093,10 @@ public final class InputMethodManager {
|
||||
+ ", ic=" + ic + ", tba=" + tba + ", handler=" + icHandler);
|
||||
}
|
||||
view.onInputConnectionOpenedInternal(ic, tba, icHandler);
|
||||
final ViewRootImpl viewRoot = view.getViewRootImpl();
|
||||
if (viewRoot != null) {
|
||||
viewRoot.getHandwritingInitiator().onInputConnectionCreated(view, tba);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -36,6 +36,7 @@ import android.util.Log;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewRootImpl;
|
||||
import android.view.inputmethod.CompletionInfo;
|
||||
import android.view.inputmethod.CorrectionInfo;
|
||||
import android.view.inputmethod.DumpableInputConnection;
|
||||
@@ -350,8 +351,19 @@ public final class RemoteInputConnectionImpl extends IInputContext.Stub {
|
||||
}
|
||||
if (handler.getLooper().isCurrentThread()) {
|
||||
servedView.onInputConnectionClosedInternal();
|
||||
final ViewRootImpl viewRoot = servedView.getViewRootImpl();
|
||||
if (viewRoot != null) {
|
||||
viewRoot.getHandwritingInitiator().onInputConnectionClosed(servedView);
|
||||
}
|
||||
} else {
|
||||
handler.post(servedView::onInputConnectionClosedInternal);
|
||||
handler.post(() -> {
|
||||
final ViewRootImpl viewRoot = servedView.getViewRootImpl();
|
||||
if (viewRoot != null) {
|
||||
viewRoot.getHandwritingInitiator()
|
||||
.onInputConnectionClosed(servedView);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.view;
|
||||
|
||||
import static android.view.MotionEvent.ACTION_DOWN;
|
||||
import static android.view.MotionEvent.ACTION_MOVE;
|
||||
import static android.view.MotionEvent.ACTION_UP;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Tests for {@link HandwritingInitiator}
|
||||
*
|
||||
* Build/Install/Run:
|
||||
* atest FrameworksCoreTests:HandwritingInitiatorTest
|
||||
*/
|
||||
@Presubmit
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class HandwritingInitiatorTest {
|
||||
private static final int TOUCH_SLOP = 8;
|
||||
private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
|
||||
private static final Rect sHwArea = new Rect(100, 200, 500, 500);
|
||||
private static final EditorInfo sFakeEditorInfo = new EditorInfo();
|
||||
|
||||
private HandwritingInitiator mHandwritingInitiator;
|
||||
private View mTestView;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
Context context = mInstrumentation.getTargetContext();
|
||||
ViewConfiguration viewConfiguration = mock(ViewConfiguration.class);
|
||||
when(viewConfiguration.getScaledTouchSlop()).thenReturn(TOUCH_SLOP);
|
||||
|
||||
InputMethodManager inputMethodManager = context.getSystemService(InputMethodManager.class);
|
||||
mHandwritingInitiator =
|
||||
spy(new HandwritingInitiator(viewConfiguration, inputMethodManager));
|
||||
mHandwritingInitiator.updateEditorBound(sHwArea);
|
||||
|
||||
// mock a parent so that HandwritingInitiator can get
|
||||
ViewGroup parent = new ViewGroup(context) {
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
// We don't layout this view.
|
||||
}
|
||||
@Override
|
||||
public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
|
||||
r.left = sHwArea.left;
|
||||
r.top = sHwArea.top;
|
||||
r.right = sHwArea.right;
|
||||
r.bottom = sHwArea.bottom;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
mTestView = mock(View.class);
|
||||
when(mTestView.isAttachedToWindow()).thenReturn(true);
|
||||
parent.addView(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTouchEvent_startHandwriting_when_stylusMoveOnce_withinHWArea() {
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
final int x1 = (sHwArea.left + sHwArea.right) / 2;
|
||||
final int y1 = (sHwArea.top + sHwArea.bottom) / 2;
|
||||
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent1);
|
||||
|
||||
final int x2 = x1 + TOUCH_SLOP * 2;
|
||||
final int y2 = y1;
|
||||
|
||||
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent2);
|
||||
|
||||
// Stylus movement win HandwritingArea should trigger IMM.startHandwriting once.
|
||||
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTouchEvent_startHandwritingOnce_when_stylusMoveMultiTimes_withinHWArea() {
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
final int x1 = (sHwArea.left + sHwArea.right) / 2;
|
||||
final int y1 = (sHwArea.top + sHwArea.bottom) / 2;
|
||||
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent1);
|
||||
|
||||
final int x2 = x1 + TOUCH_SLOP * 2;
|
||||
final int y2 = y1;
|
||||
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent2);
|
||||
|
||||
|
||||
final int x3 = x2 + TOUCH_SLOP * 2;
|
||||
final int y3 = y2;
|
||||
MotionEvent stylusEvent3 = createStylusEvent(ACTION_MOVE, x3, y3, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent3);
|
||||
|
||||
MotionEvent stylusEvent4 = createStylusEvent(ACTION_UP, x2, y2, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent4);
|
||||
|
||||
// It only calls startHandwriting once for each ACTION_DOWN.
|
||||
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTouchEvent_startHandwriting_inputConnectionBuiltAfterStylusMove() {
|
||||
final int x1 = (sHwArea.left + sHwArea.right) / 2;
|
||||
final int y1 = (sHwArea.top + sHwArea.bottom) / 2;
|
||||
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent1);
|
||||
|
||||
final int x2 = x1 + TOUCH_SLOP * 2;
|
||||
final int y2 = y1;
|
||||
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent2);
|
||||
|
||||
// InputConnection is created after stylus movement.
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
|
||||
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTouchEvent_notStartHandwriting_when_stylusTap_withinHWArea() {
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
final int x1 = 200;
|
||||
final int y1 = 200;
|
||||
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent1);
|
||||
|
||||
final int x2 = x1 + TOUCH_SLOP / 2;
|
||||
final int y2 = y1;
|
||||
MotionEvent stylusEvent2 = createStylusEvent(ACTION_UP, x2, y2, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent2);
|
||||
|
||||
verify(mHandwritingInitiator, never()).startHandwriting(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTouchEvent_notStartHandwriting_when_stylusMove_outOfHWArea() {
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
final int x1 = 10;
|
||||
final int y1 = 10;
|
||||
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent1);
|
||||
|
||||
final int x2 = x1 + TOUCH_SLOP * 2;
|
||||
final int y2 = y1;
|
||||
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent2);
|
||||
|
||||
verify(mHandwritingInitiator, never()).startHandwriting(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTouchEvent_notStartHandwriting_when_stylusMove_afterTapTimeOut() {
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
final int x1 = 10;
|
||||
final int y1 = 10;
|
||||
final long time1 = 10L;
|
||||
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent1);
|
||||
|
||||
final int x2 = x1 + TOUCH_SLOP * 2;
|
||||
final int y2 = y1;
|
||||
final long time2 = time1 + TAP_TIMEOUT + 10L;
|
||||
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, time2);
|
||||
mHandwritingInitiator.onTouchEvent(stylusEvent2);
|
||||
|
||||
// stylus movement is after TAP_TIMEOUT it shouldn't call startHandwriting.
|
||||
verify(mHandwritingInitiator, never()).startHandwriting(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onInputConnectionCreated_inputConnectionCreated() {
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
assertThat(mHandwritingInitiator.mConnectedView).isNotNull();
|
||||
assertThat(mHandwritingInitiator.mConnectedView.get()).isEqualTo(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onInputConnectionCreated_inputConnectionClosed() {
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
mHandwritingInitiator.onInputConnectionClosed(mTestView);
|
||||
|
||||
assertThat(mHandwritingInitiator.mConnectedView).isNull();
|
||||
assertThat(mHandwritingInitiator.mEditorBound).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onInputConnectionCreated_inputConnectionRestarted() {
|
||||
// When IMM restarts input connection, View#onInputConnectionCreatedInternal might be
|
||||
// called before View#onInputConnectionClosedInternal. As a result, we need to handle the
|
||||
// case where "one view "2 InputConnections".
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
mHandwritingInitiator.onInputConnectionCreated(mTestView, sFakeEditorInfo);
|
||||
mHandwritingInitiator.onInputConnectionClosed(mTestView);
|
||||
|
||||
assertThat(mHandwritingInitiator.mConnectedView).isNotNull();
|
||||
assertThat(mHandwritingInitiator.mConnectedView.get()).isEqualTo(mTestView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateEditorBound() {
|
||||
Rect rect = new Rect(1, 2, 3, 4);
|
||||
mHandwritingInitiator.updateEditorBound(rect);
|
||||
|
||||
assertThat(mHandwritingInitiator.mEditorBound).isEqualTo(rect);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
MotionEvent.PointerCoords[] coords = MotionEvent.PointerCoords.createArray(1);
|
||||
coords[0].x = x;
|
||||
coords[0].y = y;
|
||||
|
||||
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 */);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user