| /* |
| * 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.content.Context; |
| import android.graphics.Matrix; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.Region; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.EditText; |
| import android.widget.TextView; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * 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; |
| 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. |
| */ |
| private boolean mShowHoverIconForConnectedView = true; |
| |
| @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; |
| 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 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.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: |
| 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.mHasInitiatedHandwriting |
| || mState.mHasPreparedHandwritingDelegation; |
| } |
| |
| final long timeElapsed = |
| motionEvent.getEventTime() - mState.mStylusDownTimeInMillis; |
| if (timeElapsed > mHandwritingTimeoutInMillis) { |
| mState.mShouldInitHandwriting = false; |
| return mState.mHasInitiatedHandwriting |
| || mState.mHasPreparedHandwritingDelegation; |
| } |
| |
| 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) { |
| if (candidateView == getConnectedView()) { |
| if (!candidateView.hasFocus()) { |
| requestFocusWithoutReveal(candidateView); |
| } |
| startHandwriting(candidateView); |
| } else if (candidateView.getHandwritingDelegatorCallback() != null) { |
| String delegatePackageName = |
| candidateView.getAllowedHandwritingDelegatePackageName(); |
| if (delegatePackageName == null) { |
| delegatePackageName = candidateView.getContext().getOpPackageName(); |
| } |
| mImm.prepareStylusHandwritingDelegation( |
| candidateView, delegatePackageName); |
| candidateView.getHandwritingDelegatorCallback().run(); |
| mState.mHasPreparedHandwritingDelegation = true; |
| } else { |
| mState.mPendingConnectedView = new WeakReference<>(candidateView); |
| requestFocusWithoutReveal(candidateView); |
| } |
| } |
| } |
| return mState.mHasInitiatedHandwriting || mState.mHasPreparedHandwritingDelegation; |
| } |
| 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 (view == getConnectedView()) { |
| if (tryAcceptStylusHandwritingDelegation(view)) { |
| // A handwriting delegate view is accepted and handwriting starts; hide the |
| // hover icon. |
| mShowHoverIconForConnectedView = false; |
| } |
| } |
| } |
| |
| /** |
| * 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 (!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)) { |
| // A handwriting delegate view is accepted and handwriting starts; hide the |
| // hover icon. |
| mShowHoverIconForConnectedView = false; |
| return; |
| } |
| if (mState != null && mState.mPendingConnectedView != null |
| && mState.mPendingConnectedView.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) { |
| 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(); |
| } |
| } |
| |
| /** Starts a stylus handwriting session for the view. */ |
| @VisibleForTesting |
| public void startHandwriting(@NonNull View view) { |
| mImm.startStylusHandwriting(view); |
| mState.mHasInitiatedHandwriting = true; |
| mState.mShouldInitHandwriting = false; |
| mShowHoverIconForConnectedView = false; |
| if (view instanceof TextView) { |
| ((TextView) view).hideHint(); |
| } |
| } |
| |
| /** |
| * Starts a stylus handwriting session for the delegate view, if {@link |
| * InputMethodManager#prepareStylusHandwritingDelegation} was previously called. |
| */ |
| @VisibleForTesting |
| public boolean tryAcceptStylusHandwritingDelegation(@NonNull View view) { |
| String delegatorPackageName = |
| view.getAllowedHandwritingDelegatorPackageName(); |
| if (delegatorPackageName == null) { |
| delegatorPackageName = view.getContext().getOpPackageName(); |
| } |
| if (mImm.acceptStylusHandwritingDelegation(view, delegatorPackageName)) { |
| if (mState != null) { |
| mState.mHasInitiatedHandwriting = true; |
| mState.mShouldInitHandwriting = false; |
| } |
| if (view instanceof TextView) { |
| ((TextView) view).hideHint(); |
| } |
| return true; |
| } |
| return 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(); |
| } |
| |
| /** |
| * 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) { |
| 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 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) { |
| mCachedHoverTarget = new WeakReference<>(candidateView); |
| return candidateView; |
| } |
| } |
| |
| mCachedHoverTarget = null; |
| return null; |
| } |
| |
| private void requestFocusWithoutReveal(View view) { |
| if (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(); |
| } |
| } |
| |
| /** |
| * Given the location of the stylus event, return the best candidate view to initialize |
| * handwriting mode. |
| * |
| * @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) { |
| // 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 connectedView = getConnectedView(); |
| if (connectedView != null) { |
| Rect handwritingArea = mTempRect; |
| if (getViewHandwritingArea(connectedView, handwritingArea) |
| && isInHandwritingArea(handwritingArea, x, y, connectedView, isHover) |
| && shouldTriggerStylusHandwritingForView(connectedView)) { |
| if (!isHover && mState != null) { |
| mState.mStylusDownWithinEditorBounds = |
| contains(handwritingArea, x, y, 0f, 0f, 0f, 0f); |
| } |
| return connectedView; |
| } |
| } |
| |
| 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) |
| || !shouldTriggerStylusHandwritingForView(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 handwriting mode has already been initiated for the current MotionEvent sequence. |
| */ |
| private boolean mHasInitiatedHandwriting; |
| |
| private boolean mHasPreparedHandwritingDelegation; |
| |
| /** |
| * 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; |
| |
| /** 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; |
| mHasInitiatedHandwriting = false; |
| mHasPreparedHandwritingDelegation = 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.shouldInitiateHandwriting(); |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| } |