1137 lines
47 KiB
Java
1137 lines
47 KiB
Java
![]() |
/*
|
||
|
* 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 com.android.text.flags.Flags.handwritingCursorPosition;
|
||
|
import static com.android.text.flags.Flags.handwritingUnsupportedMessage;
|
||
|
|
||
|
import android.annotation.FlaggedApi;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.content.Context;
|
||
|
import android.graphics.Matrix;
|
||
|
import android.graphics.Rect;
|
||
|
import android.graphics.RectF;
|
||
|
import android.graphics.Region;
|
||
|
import android.text.TextUtils;
|
||
|
import android.view.inputmethod.ConnectionlessHandwritingCallback;
|
||
|
import android.view.inputmethod.CursorAnchorInfo;
|
||
|
import android.view.inputmethod.Flags;
|
||
|
import android.view.inputmethod.InputMethodManager;
|
||
|
import android.widget.EditText;
|
||
|
import android.widget.Editor;
|
||
|
import android.widget.TextView;
|
||
|
import android.widget.Toast;
|
||
|
|
||
|
import com.android.internal.R;
|
||
|
import com.android.internal.annotations.VisibleForTesting;
|
||
|
|
||
|
import java.lang.ref.WeakReference;
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Iterator;
|
||
|
import java.util.List;
|
||
|
import java.util.function.Consumer;
|
||
|
|
||
|
/**
|
||
|
* 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 maximum amount of distance a stylus touch can wander before it is considered
|
||
|
* handwriting.
|
||
|
*/
|
||
|
private final int mHandwritingSlop;
|
||
|
/**
|
||
|
* The timeout used to distinguish tap or long click from handwriting. If the stylus doesn't
|
||
|
* move before this timeout, it's not considered as handwriting.
|
||
|
*/
|
||
|
private final long mHandwritingTimeoutInMillis;
|
||
|
|
||
|
private State mState;
|
||
|
private final HandwritingAreaTracker mHandwritingAreasTracker = new HandwritingAreaTracker();
|
||
|
|
||
|
/** The reference to the View that currently has the input connection. */
|
||
|
@Nullable
|
||
|
@VisibleForTesting
|
||
|
public WeakReference<View> mConnectedView = 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;
|
||
|
|
||
|
/**
|
||
|
* The reference to the View that currently has focus.
|
||
|
* This replaces mConnecteView when {@code Flags#intitiationWithoutInputConnection()} is
|
||
|
* enabled.
|
||
|
*/
|
||
|
@Nullable
|
||
|
@VisibleForTesting
|
||
|
public WeakReference<View> mFocusedView = null;
|
||
|
|
||
|
private final InputMethodManager mImm;
|
||
|
|
||
|
private final int[] mTempLocation = new int[2];
|
||
|
|
||
|
private final Rect mTempRect = new Rect();
|
||
|
|
||
|
private final RectF mTempRectF = new RectF();
|
||
|
|
||
|
private final Region mTempRegion = new Region();
|
||
|
|
||
|
private final Matrix mTempMatrix = new Matrix();
|
||
|
|
||
|
/**
|
||
|
* The handwrite-able View that is currently the target of a hovering stylus pointer. This is
|
||
|
* used to help determine whether the handwriting PointerIcon should be shown in
|
||
|
* {@link #onResolvePointerIcon(Context, MotionEvent)} so that we can reduce the number of calls
|
||
|
* to {@link #findBestCandidateView(float, float, boolean)}.
|
||
|
*/
|
||
|
@Nullable
|
||
|
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.
|
||
|
* TODO(b/308827131): Rename to FocusedView after Flag is flipped.
|
||
|
*/
|
||
|
private boolean mShowHoverIconForConnectedView = true;
|
||
|
|
||
|
/** When flag is enabled, touched editors don't wait for InputConnection for initiation.
|
||
|
* However, delegation still waits for InputConnection.
|
||
|
*/
|
||
|
private final boolean mInitiateWithoutConnection = Flags.initiationWithoutInputConnection();
|
||
|
|
||
|
@VisibleForTesting
|
||
|
public HandwritingInitiator(@NonNull ViewConfiguration viewConfiguration,
|
||
|
@NonNull InputMethodManager inputMethodManager) {
|
||
|
mHandwritingSlop = viewConfiguration.getScaledHandwritingSlop();
|
||
|
mHandwritingTimeoutInMillis = ViewConfiguration.getLongPressTimeout();
|
||
|
mImm = inputMethodManager;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Notify the HandwritingInitiator that a new MotionEvent has arrived.
|
||
|
*
|
||
|
* <p>The return value indicates whether the event has been fully handled by the
|
||
|
* HandwritingInitiator and should not be dispatched to the view tree. This will be true for
|
||
|
* ACTION_MOVE events from a stylus gesture after handwriting mode has been initiated, in order
|
||
|
* to suppress other actions such as scrolling.
|
||
|
*
|
||
|
* <p>If HandwritingInitiator triggers the handwriting mode, a fabricated ACTION_CANCEL event
|
||
|
* will be sent to the ViewRootImpl.
|
||
|
*
|
||
|
* @param motionEvent the stylus {@link MotionEvent}
|
||
|
* @return true if the event has been fully handled by the {@link HandwritingInitiator} and
|
||
|
* should not be dispatched to the {@link View} tree, or false if the event should be dispatched
|
||
|
* to the {@link View} tree as usual
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public boolean onTouchEvent(@NonNull MotionEvent motionEvent) {
|
||
|
final int maskedAction = motionEvent.getActionMasked();
|
||
|
switch (maskedAction) {
|
||
|
case MotionEvent.ACTION_DOWN:
|
||
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||
|
mState = null;
|
||
|
if (!motionEvent.isStylusPointer()) {
|
||
|
// The motion event is not from a stylus event, ignore it.
|
||
|
return false;
|
||
|
}
|
||
|
mState = new State(motionEvent);
|
||
|
break;
|
||
|
case MotionEvent.ACTION_POINTER_UP:
|
||
|
final int pointerId = motionEvent.getPointerId(motionEvent.getActionIndex());
|
||
|
if (mState == null || pointerId != mState.mStylusPointerId) {
|
||
|
// ACTION_POINTER_UP is from another stylus pointer, ignore the event.
|
||
|
return false;
|
||
|
}
|
||
|
// 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.
|
||
|
if (mState != null) {
|
||
|
mState.mShouldInitHandwriting = false;
|
||
|
if (!mState.mHandled) {
|
||
|
// 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:
|
||
|
if (mState == null) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// 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.mExceedHandwritingSlop) {
|
||
|
return mState.mHandled;
|
||
|
}
|
||
|
|
||
|
final long timeElapsed =
|
||
|
motionEvent.getEventTime() - mState.mStylusDownTimeInMillis;
|
||
|
if (timeElapsed > mHandwritingTimeoutInMillis) {
|
||
|
mState.mShouldInitHandwriting = false;
|
||
|
return mState.mHandled;
|
||
|
}
|
||
|
|
||
|
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.mExceedHandwritingSlop = true;
|
||
|
View candidateView = findBestCandidateView(mState.mStylusDownX,
|
||
|
mState.mStylusDownY, /* isHover */ false);
|
||
|
if (candidateView != null && candidateView.isEnabled()) {
|
||
|
boolean candidateHasFocus = candidateView.hasFocus();
|
||
|
if (shouldShowHandwritingUnavailableMessageForView(candidateView)) {
|
||
|
int messagesResId = (candidateView instanceof TextView tv
|
||
|
&& tv.isAnyPasswordInputType())
|
||
|
? R.string.error_handwriting_unsupported_password
|
||
|
: R.string.error_handwriting_unsupported;
|
||
|
Toast.makeText(candidateView.getContext(), messagesResId,
|
||
|
Toast.LENGTH_SHORT).show();
|
||
|
if (!candidateView.hasFocus()) {
|
||
|
requestFocusWithoutReveal(candidateView);
|
||
|
}
|
||
|
mImm.showSoftInput(candidateView, 0);
|
||
|
mState.mHandled = true;
|
||
|
mState.mShouldInitHandwriting = false;
|
||
|
motionEvent.setAction((motionEvent.getAction()
|
||
|
& MotionEvent.ACTION_POINTER_INDEX_MASK)
|
||
|
| MotionEvent.ACTION_CANCEL);
|
||
|
candidateView.getRootView().dispatchTouchEvent(motionEvent);
|
||
|
} else if (candidateView == getConnectedOrFocusedView()) {
|
||
|
if (!candidateHasFocus) {
|
||
|
requestFocusWithoutReveal(candidateView);
|
||
|
}
|
||
|
startHandwriting(candidateView);
|
||
|
} else if (candidateView.getHandwritingDelegatorCallback() != null) {
|
||
|
prepareDelegation(candidateView);
|
||
|
} else {
|
||
|
if (mInitiateWithoutConnection) {
|
||
|
if (!candidateHasFocus) {
|
||
|
// schedule for view focus.
|
||
|
mState.mPendingFocusedView = new WeakReference<>(candidateView);
|
||
|
requestFocusWithoutReveal(candidateView);
|
||
|
}
|
||
|
} else {
|
||
|
mState.mPendingConnectedView = new WeakReference<>(candidateView);
|
||
|
if (!candidateHasFocus) {
|
||
|
requestFocusWithoutReveal(candidateView);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return mState.mHandled;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private View getConnectedView() {
|
||
|
if (mConnectedView == null) return null;
|
||
|
return mConnectedView.get();
|
||
|
}
|
||
|
|
||
|
private void clearConnectedView() {
|
||
|
mConnectedView = null;
|
||
|
mConnectionCount = 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Notify HandwritingInitiator that a delegate view (see {@link View#isHandwritingDelegate})
|
||
|
* gained focus.
|
||
|
*/
|
||
|
public void onDelegateViewFocused(@NonNull View view) {
|
||
|
if (mInitiateWithoutConnection) {
|
||
|
onEditorFocused(view);
|
||
|
}
|
||
|
if (view == getConnectedView()) {
|
||
|
tryAcceptStylusHandwritingDelegation(view);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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) {
|
||
|
if (mInitiateWithoutConnection && !view.isHandwritingDelegate()) {
|
||
|
// When flag is enabled, only delegation continues to wait for InputConnection.
|
||
|
return;
|
||
|
}
|
||
|
if (!view.isAutoHandwritingEnabled()) {
|
||
|
clearConnectedView();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
final View connectedView = getConnectedView();
|
||
|
if (connectedView == view) {
|
||
|
++mConnectionCount;
|
||
|
} 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)) {
|
||
|
// tryAcceptStylusHandwritingDelegation should set boolean below, however, we
|
||
|
// cannot mock IMM to return true for acceptStylusDelegation().
|
||
|
// TODO(b/324670412): we should move any dependent tests to integration and remove
|
||
|
// the assignment below.
|
||
|
mShowHoverIconForConnectedView = false;
|
||
|
return;
|
||
|
}
|
||
|
if (!mInitiateWithoutConnection && mState != null
|
||
|
&& mState.mPendingConnectedView != null
|
||
|
&& mState.mPendingConnectedView.get() == view) {
|
||
|
startHandwriting(view);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Notify HandwritingInitiator that a new editor is focused.
|
||
|
* @param view the view that received focus.
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public void onEditorFocused(@NonNull View view) {
|
||
|
if (!mInitiateWithoutConnection) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!view.isAutoHandwritingEnabled()) {
|
||
|
clearFocusedView(view);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
final View focusedView = getFocusedView();
|
||
|
if (focusedView == view) {
|
||
|
return;
|
||
|
}
|
||
|
updateFocusedView(view);
|
||
|
|
||
|
if (mState != null && mState.mPendingFocusedView != null
|
||
|
&& mState.mPendingFocusedView.get() == view) {
|
||
|
startHandwriting(view);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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) {
|
||
|
if (mInitiateWithoutConnection && !view.isHandwritingDelegate()) {
|
||
|
return;
|
||
|
}
|
||
|
final View connectedView = getConnectedView();
|
||
|
if (connectedView == null) return;
|
||
|
if (connectedView == view) {
|
||
|
--mConnectionCount;
|
||
|
if (mConnectionCount == 0) {
|
||
|
clearConnectedView();
|
||
|
}
|
||
|
} else {
|
||
|
// Unexpected branch, set mConnectedView to null to avoid further problem.
|
||
|
clearConnectedView();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private View getFocusedView() {
|
||
|
if (mFocusedView == null) return null;
|
||
|
return mFocusedView.get();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clear the tracked focused view tracked for handwriting initiation.
|
||
|
* @param view the focused view.
|
||
|
*/
|
||
|
public void clearFocusedView(View view) {
|
||
|
if (view == null || mFocusedView == null) {
|
||
|
return;
|
||
|
}
|
||
|
if (mFocusedView.get() == view) {
|
||
|
mFocusedView = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called when new {@link Editor} is focused.
|
||
|
* @return {@code true} if handwriting can initiate for given view.
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public boolean updateFocusedView(@NonNull View view) {
|
||
|
if (!view.shouldInitiateHandwriting()) {
|
||
|
mFocusedView = null;
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
final View focusedView = getFocusedView();
|
||
|
if (focusedView != view) {
|
||
|
mFocusedView = new WeakReference<>(view);
|
||
|
// A new view just gain focus. By default, we should show hover icon for it.
|
||
|
mShowHoverIconForConnectedView = true;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/** Starts a stylus handwriting session for the view. */
|
||
|
@VisibleForTesting
|
||
|
public void startHandwriting(@NonNull View view) {
|
||
|
mImm.startStylusHandwriting(view);
|
||
|
mState.mHandled = true;
|
||
|
mState.mShouldInitHandwriting = false;
|
||
|
mShowHoverIconForConnectedView = false;
|
||
|
if (view instanceof TextView) {
|
||
|
((TextView) view).hideHint();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void prepareDelegation(View view) {
|
||
|
String delegatePackageName = view.getAllowedHandwritingDelegatePackageName();
|
||
|
if (delegatePackageName == null) {
|
||
|
delegatePackageName = view.getContext().getOpPackageName();
|
||
|
}
|
||
|
if (mImm.isConnectionlessStylusHandwritingAvailable()) {
|
||
|
// No other view should have focus during the connectionless handwriting session, as
|
||
|
// this could cause user confusion about the input target for the session.
|
||
|
view.getViewRootImpl().getView().clearFocus();
|
||
|
mImm.startConnectionlessStylusHandwritingForDelegation(
|
||
|
view, getCursorAnchorInfoForConnectionless(view), delegatePackageName,
|
||
|
view::post, new DelegationCallback(view, delegatePackageName));
|
||
|
mState.mShouldInitHandwriting = false;
|
||
|
} else {
|
||
|
mImm.prepareStylusHandwritingDelegation(view, delegatePackageName);
|
||
|
view.getHandwritingDelegatorCallback().run();
|
||
|
}
|
||
|
mState.mHandled = true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Starts a stylus handwriting session for the delegate view, if {@link
|
||
|
* InputMethodManager#prepareStylusHandwritingDelegation} was previously called.
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public boolean tryAcceptStylusHandwritingDelegation(@NonNull View view) {
|
||
|
if (Flags.useZeroJankProxy()) {
|
||
|
tryAcceptStylusHandwritingDelegationAsync(view);
|
||
|
} else {
|
||
|
return tryAcceptStylusHandwritingDelegationInternal(view);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
private boolean tryAcceptStylusHandwritingDelegationInternal(@NonNull View view) {
|
||
|
String delegatorPackageName =
|
||
|
view.getAllowedHandwritingDelegatorPackageName();
|
||
|
if (delegatorPackageName == null) {
|
||
|
delegatorPackageName = view.getContext().getOpPackageName();
|
||
|
}
|
||
|
if (mImm.acceptStylusHandwritingDelegation(view, delegatorPackageName)) {
|
||
|
onDelegationAccepted(view);
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
@FlaggedApi(Flags.FLAG_USE_ZERO_JANK_PROXY)
|
||
|
private void tryAcceptStylusHandwritingDelegationAsync(@NonNull View view) {
|
||
|
String delegatorPackageName =
|
||
|
view.getAllowedHandwritingDelegatorPackageName();
|
||
|
if (delegatorPackageName == null) {
|
||
|
delegatorPackageName = view.getContext().getOpPackageName();
|
||
|
}
|
||
|
Consumer<Boolean> consumer = delegationAccepted -> {
|
||
|
if (delegationAccepted) {
|
||
|
onDelegationAccepted(view);
|
||
|
}
|
||
|
};
|
||
|
mImm.acceptStylusHandwritingDelegation(view, delegatorPackageName, view::post, consumer);
|
||
|
}
|
||
|
|
||
|
private void onDelegationAccepted(View view) {
|
||
|
if (mState != null) {
|
||
|
mState.mHandled = true;
|
||
|
mState.mShouldInitHandwriting = false;
|
||
|
}
|
||
|
if (view instanceof TextView) {
|
||
|
((TextView) view).hideHint();
|
||
|
}
|
||
|
// A handwriting delegate view is accepted and handwriting starts; hide the
|
||
|
// hover icon.
|
||
|
mShowHoverIconForConnectedView = false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Notify that the handwriting area for the given view might be updated.
|
||
|
* @param view the view whose handwriting area might be updated.
|
||
|
*/
|
||
|
public void updateHandwritingAreasForView(@NonNull View view) {
|
||
|
mHandwritingAreasTracker.updateHandwritingAreaForView(view);
|
||
|
}
|
||
|
|
||
|
private static boolean shouldTriggerStylusHandwritingForView(@NonNull View view) {
|
||
|
if (!view.shouldInitiateHandwriting()) {
|
||
|
return false;
|
||
|
}
|
||
|
// The view may be a handwriting initiation delegator, in which case it is not the editor
|
||
|
// view for which handwriting would be started. However, in almost all cases, the return
|
||
|
// values of View#isStylusHandwritingAvailable will be the same for the delegator view and
|
||
|
// the delegate editor view. So the delegator view can be used to decide whether handwriting
|
||
|
// should be triggered.
|
||
|
return view.isStylusHandwritingAvailable();
|
||
|
}
|
||
|
|
||
|
private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) {
|
||
|
return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view);
|
||
|
}
|
||
|
|
||
|
private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView(
|
||
|
@NonNull View view) {
|
||
|
return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the pointer icon for the motion event, or null if it doesn't specify the icon.
|
||
|
* This gives HandwritingInitiator a chance to show the stylus handwriting icon over a
|
||
|
* handwrite-able area.
|
||
|
*/
|
||
|
public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) {
|
||
|
final View hoverView = findHoverView(event);
|
||
|
if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (mShowHoverIconForConnectedView) {
|
||
|
return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING);
|
||
|
}
|
||
|
|
||
|
if (hoverView != getConnectedOrFocusedView()) {
|
||
|
// The stylus is hovering on another view that supports handwriting. We should show
|
||
|
// hover icon. Also reset the mShowHoverIconForFocusedView so that hover
|
||
|
// icon is displayed again next time when the stylus hovers on focused view.
|
||
|
mShowHoverIconForConnectedView = true;
|
||
|
return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// TODO(b/308827131): Remove once Flag is flipped.
|
||
|
private View getConnectedOrFocusedView() {
|
||
|
if (mInitiateWithoutConnection) {
|
||
|
return mFocusedView == null ? null : mFocusedView.get();
|
||
|
} else {
|
||
|
return mConnectedView == null ? null : mConnectedView.get();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private View getCachedHoverTarget() {
|
||
|
if (mCachedHoverTarget == null) {
|
||
|
return null;
|
||
|
}
|
||
|
return mCachedHoverTarget.get();
|
||
|
}
|
||
|
|
||
|
private View findHoverView(MotionEvent event) {
|
||
|
if (!event.isStylusPointer() || !event.isHoverEvent()) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
|
||
|
|| event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
|
||
|
final float hoverX = event.getX(event.getActionIndex());
|
||
|
final float hoverY = event.getY(event.getActionIndex());
|
||
|
|
||
|
final View cachedHoverTarget = getCachedHoverTarget();
|
||
|
if (cachedHoverTarget != null) {
|
||
|
final Rect handwritingArea = mTempRect;
|
||
|
if (getViewHandwritingArea(cachedHoverTarget, handwritingArea)
|
||
|
&& isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget,
|
||
|
/* isHover */ true)
|
||
|
&& shouldTriggerStylusHandwritingForView(cachedHoverTarget)) {
|
||
|
return cachedHoverTarget;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true);
|
||
|
|
||
|
if (candidateView != null) {
|
||
|
if (!handwritingUnsupportedMessage()) {
|
||
|
mCachedHoverTarget = new WeakReference<>(candidateView);
|
||
|
}
|
||
|
return candidateView;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
mCachedHoverTarget = null;
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private void requestFocusWithoutReveal(View view) {
|
||
|
if (!handwritingCursorPosition() && view instanceof EditText editText
|
||
|
&& !mState.mStylusDownWithinEditorBounds) {
|
||
|
// If the stylus down point was inside the EditText's bounds, then the EditText will
|
||
|
// automatically set its cursor position nearest to the stylus down point when it
|
||
|
// gains focus. If the stylus down point was outside the EditText's bounds (within
|
||
|
// the extended handwriting bounds), then we must calculate and set the cursor
|
||
|
// position manually.
|
||
|
view.getLocationInWindow(mTempLocation);
|
||
|
int offset = editText.getOffsetForPosition(
|
||
|
mState.mStylusDownX - mTempLocation[0],
|
||
|
mState.mStylusDownY - mTempLocation[1]);
|
||
|
editText.setSelection(offset);
|
||
|
}
|
||
|
if (view.getRevealOnFocusHint()) {
|
||
|
view.setRevealOnFocusHint(false);
|
||
|
view.requestFocus();
|
||
|
view.setRevealOnFocusHint(true);
|
||
|
} else {
|
||
|
view.requestFocus();
|
||
|
}
|
||
|
if (handwritingCursorPosition() && view instanceof EditText editText) {
|
||
|
// Move the cursor to the end of the paragraph closest to the stylus down point.
|
||
|
view.getLocationInWindow(mTempLocation);
|
||
|
int line = editText.getLineAtCoordinate(mState.mStylusDownY - mTempLocation[1]);
|
||
|
int paragraphEnd = TextUtils.indexOf(editText.getText(), '\n',
|
||
|
editText.getLayout().getLineStart(line));
|
||
|
if (paragraphEnd < 0) {
|
||
|
paragraphEnd = editText.getText().length();
|
||
|
}
|
||
|
editText.setSelection(paragraphEnd);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given the location of the stylus event, return the best candidate view to initialize
|
||
|
* handwriting mode or show the handwriting unavailable error message.
|
||
|
*
|
||
|
* @param x the x coordinates of the stylus event, in the coordinates of the window.
|
||
|
* @param y the y coordinates of the stylus event, in the coordinates of the window.
|
||
|
*/
|
||
|
@Nullable
|
||
|
private View findBestCandidateView(float x, float y, boolean isHover) {
|
||
|
// TODO(b/308827131): Rename to FocusedView after Flag is flipped.
|
||
|
// If the connectedView is not null and do not set any handwriting area, it will check
|
||
|
// whether the connectedView's boundary contains the initial stylus position. If true,
|
||
|
// directly return the connectedView.
|
||
|
final View connectedOrFocusedView = getConnectedOrFocusedView();
|
||
|
if (connectedOrFocusedView != null) {
|
||
|
Rect handwritingArea = mTempRect;
|
||
|
if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea)
|
||
|
&& isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover)
|
||
|
&& shouldTriggerHandwritingOrShowUnavailableMessageForView(
|
||
|
connectedOrFocusedView)) {
|
||
|
if (!isHover && mState != null) {
|
||
|
mState.mStylusDownWithinEditorBounds =
|
||
|
contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
|
||
|
}
|
||
|
return connectedOrFocusedView;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
float minDistance = Float.MAX_VALUE;
|
||
|
View bestCandidate = null;
|
||
|
// Check the registered handwriting areas.
|
||
|
final List<HandwritableViewInfo> handwritableViewInfos =
|
||
|
mHandwritingAreasTracker.computeViewInfos();
|
||
|
for (HandwritableViewInfo viewInfo : handwritableViewInfos) {
|
||
|
final View view = viewInfo.getView();
|
||
|
final Rect handwritingArea = viewInfo.getHandwritingArea();
|
||
|
if (!isInHandwritingArea(handwritingArea, x, y, view, isHover)
|
||
|
|| !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
final float distance = distance(handwritingArea, x, y);
|
||
|
if (distance == 0f) {
|
||
|
if (!isHover && mState != null) {
|
||
|
mState.mStylusDownWithinEditorBounds = true;
|
||
|
}
|
||
|
return view;
|
||
|
}
|
||
|
if (distance < minDistance) {
|
||
|
minDistance = distance;
|
||
|
bestCandidate = view;
|
||
|
}
|
||
|
}
|
||
|
return bestCandidate;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the square of the distance from point (x, y) to the given rect, which is mainly used
|
||
|
* for comparison. The distance is defined to be: the shortest distance between (x, y) to any
|
||
|
* point on rect. When (x, y) is contained by the rect, return 0f.
|
||
|
*/
|
||
|
private static float distance(@NonNull Rect rect, float x, float y) {
|
||
|
if (contains(rect, x, y, 0f, 0f, 0f, 0f)) {
|
||
|
return 0f;
|
||
|
}
|
||
|
|
||
|
/* The distance between point (x, y) and rect, there are 2 basic cases:
|
||
|
* a) The distance is the distance from (x, y) to the closest corner on rect.
|
||
|
* o | |
|
||
|
* ---+-----+---
|
||
|
* | |
|
||
|
* ---+-----+---
|
||
|
* | |
|
||
|
* b) The distance is the distance from (x, y) to the closest edge on rect.
|
||
|
* | o |
|
||
|
* ---+-----+---
|
||
|
* | |
|
||
|
* ---+-----+---
|
||
|
* | |
|
||
|
* We define xDistance as following(similar for yDistance):
|
||
|
* If x is in [left, right) 0, else min(abs(x - left), abs(x - y))
|
||
|
* For case a, sqrt(xDistance^2 + yDistance^2) is the final distance.
|
||
|
* For case b, distance should be yDistance, which is also equal to
|
||
|
* sqrt(xDistance^2 + yDistance^2) because xDistance is 0.
|
||
|
*/
|
||
|
final float xDistance;
|
||
|
if (x >= rect.left && x < rect.right) {
|
||
|
xDistance = 0f;
|
||
|
} else if (x < rect.left) {
|
||
|
xDistance = rect.left - x;
|
||
|
} else {
|
||
|
xDistance = x - rect.right;
|
||
|
}
|
||
|
|
||
|
final float yDistance;
|
||
|
if (y >= rect.top && y < rect.bottom) {
|
||
|
yDistance = 0f;
|
||
|
} else if (y < rect.top) {
|
||
|
yDistance = rect.top - y;
|
||
|
} else {
|
||
|
yDistance = y - rect.bottom;
|
||
|
}
|
||
|
// We can omit sqrt here because we only need the distance for comparison.
|
||
|
return xDistance * xDistance + yDistance * yDistance;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the handwriting area of the given view, represented in the window's coordinate.
|
||
|
* If the view didn't set any handwriting area, it will return the view's boundary.
|
||
|
*
|
||
|
* <p> The handwriting area is clipped to its visible part.
|
||
|
* Notice that the returned rectangle is the view's original handwriting area without the
|
||
|
* view's handwriting area extends. </p>
|
||
|
*
|
||
|
* @param view the {@link View} whose handwriting area we want to compute.
|
||
|
* @param rect the {@link Rect} to receive the result.
|
||
|
*
|
||
|
* @return true if the view's handwriting area is still visible, or false if it's clipped and
|
||
|
* fully invisible. This method only consider the clip by given view's parents, but not the case
|
||
|
* where a view is covered by its sibling view.
|
||
|
*/
|
||
|
private static boolean getViewHandwritingArea(@NonNull View view, @NonNull Rect rect) {
|
||
|
final ViewParent viewParent = view.getParent();
|
||
|
if (viewParent != null && view.isAttachedToWindow() && view.isAggregatedVisible()) {
|
||
|
final Rect localHandwritingArea = view.getHandwritingArea();
|
||
|
if (localHandwritingArea != null) {
|
||
|
rect.set(localHandwritingArea);
|
||
|
} else {
|
||
|
rect.set(0, 0, view.getWidth(), view.getHeight());
|
||
|
}
|
||
|
return viewParent.getChildVisibleRect(view, rect, null);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return true if the (x, y) is inside by the given {@link Rect} with the View's
|
||
|
* handwriting bounds with offsets applied.
|
||
|
*/
|
||
|
private boolean isInHandwritingArea(@Nullable Rect handwritingArea,
|
||
|
float x, float y, View view, boolean isHover) {
|
||
|
if (handwritingArea == null) return false;
|
||
|
|
||
|
if (!contains(handwritingArea, x, y,
|
||
|
view.getHandwritingBoundsOffsetLeft(),
|
||
|
view.getHandwritingBoundsOffsetTop(),
|
||
|
view.getHandwritingBoundsOffsetRight(),
|
||
|
view.getHandwritingBoundsOffsetBottom())) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// The returned handwritingArea computed by ViewParent#getChildVisibleRect didn't consider
|
||
|
// the case where a view is stacking on top of the editor. (e.g. DrawerLayout, popup)
|
||
|
// We must check the hit region of the editor again, and avoid the case where another
|
||
|
// view on top of the editor is handling MotionEvents.
|
||
|
ViewParent parent = view.getParent();
|
||
|
if (parent == null) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
Region region = mTempRegion;
|
||
|
mTempRegion.set(0, 0, view.getWidth(), view.getHeight());
|
||
|
Matrix matrix = mTempMatrix;
|
||
|
matrix.reset();
|
||
|
if (!parent.getChildLocalHitRegion(view, region, matrix, isHover)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// It's not easy to extend the region by the given handwritingBoundsOffset. Instead, we
|
||
|
// create a rectangle surrounding the motion event location and check if this rectangle
|
||
|
// overlaps with the hit region of the editor.
|
||
|
float left = x - view.getHandwritingBoundsOffsetRight();
|
||
|
float top = y - view.getHandwritingBoundsOffsetBottom();
|
||
|
float right = Math.max(x + view.getHandwritingBoundsOffsetLeft(), left + 1);
|
||
|
float bottom = Math.max(y + view.getHandwritingBoundsOffsetTop(), top + 1);
|
||
|
RectF rectF = mTempRectF;
|
||
|
rectF.set(left, top, right, bottom);
|
||
|
matrix.mapRect(rectF);
|
||
|
|
||
|
return region.op(Math.round(rectF.left), Math.round(rectF.top),
|
||
|
Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return true if the (x, y) is inside by the given {@link Rect} offset by the given
|
||
|
* offsetLeft, offsetTop, offsetRight and offsetBottom.
|
||
|
*/
|
||
|
private static boolean contains(@NonNull Rect rect, float x, float y,
|
||
|
float offsetLeft, float offsetTop, float offsetRight, float offsetBottom) {
|
||
|
return x >= rect.left - offsetLeft && x < rect.right + offsetRight
|
||
|
&& y >= rect.top - offsetTop && y < rect.bottom + offsetBottom;
|
||
|
}
|
||
|
|
||
|
private boolean largerThanTouchSlop(float x1, float y1, float x2, float y2) {
|
||
|
float dx = x1 - x2;
|
||
|
float dy = y1 - y2;
|
||
|
return dx * dx + dy * dy > mHandwritingSlop * mHandwritingSlop;
|
||
|
}
|
||
|
|
||
|
/** 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;
|
||
|
|
||
|
/**
|
||
|
* Whether the current MotionEvent sequence has been handled by the handwriting initiator,
|
||
|
* either by initiating handwriting mode, or by preparing handwriting delegation.
|
||
|
*/
|
||
|
private boolean mHandled;
|
||
|
|
||
|
/**
|
||
|
* Whether the current ongoing stylus MotionEvent sequence already exceeds the
|
||
|
* handwriting slop.
|
||
|
* It's used for the case where the stylus exceeds handwriting slop before the target View
|
||
|
* built InputConnection.
|
||
|
*/
|
||
|
private boolean mExceedHandwritingSlop;
|
||
|
|
||
|
/**
|
||
|
* Whether the stylus down point of the MotionEvent sequence was within the editor's bounds
|
||
|
* (not including the extended handwriting bounds).
|
||
|
*/
|
||
|
private boolean mStylusDownWithinEditorBounds;
|
||
|
|
||
|
/**
|
||
|
* A view which has requested focus and is pending input connection creation. When an input
|
||
|
* connection is created for the view, a handwriting session should be started for the view.
|
||
|
*/
|
||
|
private WeakReference<View> mPendingConnectedView = null;
|
||
|
|
||
|
/**
|
||
|
* A view which has requested focus and is yet to receive it.
|
||
|
* When view receives focus, a handwriting session should be started for the view.
|
||
|
*/
|
||
|
private WeakReference<View> mPendingFocusedView = null;
|
||
|
|
||
|
/** The pointer id of the stylus pointer that is being tracked. */
|
||
|
private final int mStylusPointerId;
|
||
|
/** The time stamp when the stylus pointer goes down. */
|
||
|
private final long mStylusDownTimeInMillis;
|
||
|
/** The initial location where the stylus pointer goes down. */
|
||
|
private final float mStylusDownX;
|
||
|
private final float mStylusDownY;
|
||
|
|
||
|
private State(MotionEvent motionEvent) {
|
||
|
final int actionIndex = motionEvent.getActionIndex();
|
||
|
mStylusPointerId = motionEvent.getPointerId(actionIndex);
|
||
|
mStylusDownTimeInMillis = motionEvent.getEventTime();
|
||
|
mStylusDownX = motionEvent.getX(actionIndex);
|
||
|
mStylusDownY = motionEvent.getY(actionIndex);
|
||
|
|
||
|
mShouldInitHandwriting = true;
|
||
|
mHandled = false;
|
||
|
mExceedHandwritingSlop = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** The helper method to check if the given view is still active for handwriting. */
|
||
|
private static boolean isViewActive(@Nullable View view) {
|
||
|
return view != null && view.isAttachedToWindow() && view.isAggregatedVisible()
|
||
|
&& view.shouldTrackHandwritingArea();
|
||
|
}
|
||
|
|
||
|
private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) {
|
||
|
CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
|
||
|
// Fake editor views will usually display hint text. The hint text view can be used to
|
||
|
// populate the CursorAnchorInfo.
|
||
|
TextView textView = findFirstTextViewDescendent(view);
|
||
|
if (textView != null) {
|
||
|
textView.getCursorAnchorInfo(0, builder, mTempMatrix);
|
||
|
if (textView.getSelectionStart() < 0) {
|
||
|
// Insertion marker location is not populated if selection start is negative, so
|
||
|
// make a best guess.
|
||
|
float bottom = textView.getHeight() - textView.getExtendedPaddingBottom();
|
||
|
builder.setInsertionMarkerLocation(
|
||
|
/* horizontalPosition= */ textView.getCompoundPaddingStart(),
|
||
|
/* lineTop= */ textView.getExtendedPaddingTop(),
|
||
|
/* lineBaseline= */ bottom,
|
||
|
/* lineBottom= */ bottom,
|
||
|
/* flags= */ 0);
|
||
|
}
|
||
|
} else {
|
||
|
// If there is no TextView descendent, just populate the insertion marker with the start
|
||
|
// edge of the view.
|
||
|
mTempMatrix.reset();
|
||
|
view.transformMatrixToGlobal(mTempMatrix);
|
||
|
builder.setMatrix(mTempMatrix);
|
||
|
builder.setInsertionMarkerLocation(
|
||
|
/* horizontalPosition= */ view.isLayoutRtl() ? view.getWidth() : 0,
|
||
|
/* lineTop= */ 0,
|
||
|
/* lineBaseline= */ view.getHeight(),
|
||
|
/* lineBottom= */ view.getHeight(),
|
||
|
/* flags= */ 0);
|
||
|
}
|
||
|
return builder.build();
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private static TextView findFirstTextViewDescendent(View view) {
|
||
|
if (view instanceof ViewGroup viewGroup) {
|
||
|
TextView textView;
|
||
|
for (int i = 0; i < viewGroup.getChildCount(); ++i) {
|
||
|
View child = viewGroup.getChildAt(i);
|
||
|
textView = (child instanceof TextView tv)
|
||
|
? tv : findFirstTextViewDescendent(viewGroup.getChildAt(i));
|
||
|
if (textView != null
|
||
|
&& textView.isAggregatedVisible()
|
||
|
&& (!TextUtils.isEmpty(textView.getText())
|
||
|
|| !TextUtils.isEmpty(textView.getHint()))) {
|
||
|
return textView;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A class used to track the handwriting areas set by the Views.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public static class HandwritingAreaTracker {
|
||
|
private final List<HandwritableViewInfo> mHandwritableViewInfos;
|
||
|
|
||
|
public HandwritingAreaTracker() {
|
||
|
mHandwritableViewInfos = new ArrayList<>();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Notify this tracker that the handwriting area of the given view has been updated.
|
||
|
* This method does three things:
|
||
|
* a) iterate over the all the tracked ViewInfos and remove those already invalid ones.
|
||
|
* b) mark the given view's ViewInfo to be dirty. So that next time when
|
||
|
* {@link #computeViewInfos} is called, this view's handwriting area will be recomputed.
|
||
|
* c) If no the given view is not in the tracked ViewInfo list, a new ViewInfo object will
|
||
|
* be created and added to the list.
|
||
|
*
|
||
|
* @param view the view whose handwriting area is updated.
|
||
|
*/
|
||
|
public void updateHandwritingAreaForView(@NonNull View view) {
|
||
|
Iterator<HandwritableViewInfo> iterator = mHandwritableViewInfos.iterator();
|
||
|
boolean found = false;
|
||
|
while (iterator.hasNext()) {
|
||
|
final HandwritableViewInfo handwritableViewInfo = iterator.next();
|
||
|
final View curView = handwritableViewInfo.getView();
|
||
|
if (!isViewActive(curView)) {
|
||
|
iterator.remove();
|
||
|
}
|
||
|
if (curView == view) {
|
||
|
found = true;
|
||
|
handwritableViewInfo.mIsDirty = true;
|
||
|
}
|
||
|
}
|
||
|
if (!found && isViewActive(view)) {
|
||
|
// The given view is not tracked. Create a new HandwritableViewInfo for it and add
|
||
|
// to the list.
|
||
|
mHandwritableViewInfos.add(new HandwritableViewInfo(view));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Update the handwriting areas and return a list of ViewInfos containing the view
|
||
|
* reference and its handwriting area.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public List<HandwritableViewInfo> computeViewInfos() {
|
||
|
mHandwritableViewInfos.removeIf(viewInfo -> !viewInfo.update());
|
||
|
return mHandwritableViewInfos;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A class that reference to a View and its handwriting area(in the ViewRoot's coordinate.)
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public static class HandwritableViewInfo {
|
||
|
final WeakReference<View> mViewRef;
|
||
|
Rect mHandwritingArea = null;
|
||
|
@VisibleForTesting
|
||
|
public boolean mIsDirty = true;
|
||
|
|
||
|
@VisibleForTesting
|
||
|
public HandwritableViewInfo(@NonNull View view) {
|
||
|
mViewRef = new WeakReference<>(view);
|
||
|
}
|
||
|
|
||
|
/** Return the tracked view. */
|
||
|
@Nullable
|
||
|
public View getView() {
|
||
|
return mViewRef.get();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the tracked handwriting area, represented in the ViewRoot's coordinates.
|
||
|
* Notice, the caller should not modify the returned Rect.
|
||
|
*/
|
||
|
@Nullable
|
||
|
public Rect getHandwritingArea() {
|
||
|
return mHandwritingArea;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Update the handwriting area in this ViewInfo.
|
||
|
*
|
||
|
* @return true if this ViewInfo is still valid. Or false if this ViewInfo has become
|
||
|
* invalid due to either view is no longer visible, or the handwriting area set by the
|
||
|
* view is removed. {@link HandwritingAreaTracker} no longer need to keep track of this
|
||
|
* HandwritableViewInfo this method returns false.
|
||
|
*/
|
||
|
public boolean update() {
|
||
|
final View view = getView();
|
||
|
if (!isViewActive(view)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!mIsDirty) {
|
||
|
return true;
|
||
|
}
|
||
|
final Rect handwritingArea = view.getHandwritingArea();
|
||
|
if (handwritingArea == null) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
ViewParent parent = view.getParent();
|
||
|
if (parent != null) {
|
||
|
if (mHandwritingArea == null) {
|
||
|
mHandwritingArea = new Rect();
|
||
|
}
|
||
|
mHandwritingArea.set(handwritingArea);
|
||
|
if (!parent.getChildVisibleRect(view, mHandwritingArea, null /* offset */)) {
|
||
|
mHandwritingArea = null;
|
||
|
}
|
||
|
}
|
||
|
mIsDirty = false;
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private class DelegationCallback implements ConnectionlessHandwritingCallback {
|
||
|
private final View mView;
|
||
|
private final String mDelegatePackageName;
|
||
|
|
||
|
private DelegationCallback(View view, String delegatePackageName) {
|
||
|
mView = view;
|
||
|
mDelegatePackageName = delegatePackageName;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onResult(@NonNull CharSequence text) {
|
||
|
mView.getHandwritingDelegatorCallback().run();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onError(int errorCode) {
|
||
|
switch (errorCode) {
|
||
|
case CONNECTIONLESS_HANDWRITING_ERROR_NO_TEXT_RECOGNIZED:
|
||
|
mView.getHandwritingDelegatorCallback().run();
|
||
|
break;
|
||
|
case CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED:
|
||
|
// Fall back to the old delegation flow
|
||
|
mImm.prepareStylusHandwritingDelegation(mView, mDelegatePackageName);
|
||
|
mView.getHandwritingDelegatorCallback().run();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|