1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.graphics.Matrix; 23 import android.graphics.Rect; 24 import android.graphics.RectF; 25 import android.graphics.Region; 26 import android.view.inputmethod.InputMethodManager; 27 import android.widget.EditText; 28 import android.widget.TextView; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 32 import java.lang.ref.WeakReference; 33 import java.util.ArrayList; 34 import java.util.Iterator; 35 import java.util.List; 36 37 /** 38 * Initiates handwriting mode once it detects stylus movement in handwritable areas. 39 * 40 * It is designed to be used by {@link ViewRootImpl}. For every stylus related MotionEvent that is 41 * dispatched to view tree, ViewRootImpl should call {@link #onTouchEvent} method of this class. 42 * And it will automatically request to enter the handwriting mode when the conditions meet. 43 * 44 * Notice that ViewRootImpl should still dispatch MotionEvents to view tree as usual. 45 * And if it successfully enters the handwriting mode, the ongoing MotionEvent stream will be 46 * routed to the input method. Input system will fabricate an ACTION_CANCEL and send to 47 * ViewRootImpl. 48 * 49 * This class does nothing if: 50 * a) MotionEvents are not from stylus. 51 * b) The user taps or long-clicks with a stylus etc. 52 * c) Stylus pointer down position is not within a handwritable area. 53 * 54 * Used by InputMethodManager. 55 * @hide 56 */ 57 public class HandwritingInitiator { 58 /** 59 * The maximum amount of distance a stylus touch can wander before it is considered 60 * handwriting. 61 */ 62 private final int mHandwritingSlop; 63 /** 64 * The timeout used to distinguish tap or long click from handwriting. If the stylus doesn't 65 * move before this timeout, it's not considered as handwriting. 66 */ 67 private final long mHandwritingTimeoutInMillis; 68 69 private State mState; 70 private final HandwritingAreaTracker mHandwritingAreasTracker = new HandwritingAreaTracker(); 71 72 /** The reference to the View that currently has the input connection. */ 73 @Nullable 74 @VisibleForTesting 75 public WeakReference<View> mConnectedView = null; 76 77 /** 78 * When InputConnection restarts for a View, View#onInputConnectionCreatedInternal 79 * might be called before View#onInputConnectionClosedInternal, so we need to count the input 80 * connections and only set mConnectedView to null when mConnectionCount is zero. 81 */ 82 private int mConnectionCount = 0; 83 private final InputMethodManager mImm; 84 85 private final int[] mTempLocation = new int[2]; 86 87 private final Rect mTempRect = new Rect(); 88 89 private final RectF mTempRectF = new RectF(); 90 91 private final Region mTempRegion = new Region(); 92 93 private final Matrix mTempMatrix = new Matrix(); 94 95 /** 96 * The handwrite-able View that is currently the target of a hovering stylus pointer. This is 97 * used to help determine whether the handwriting PointerIcon should be shown in 98 * {@link #onResolvePointerIcon(Context, MotionEvent)} so that we can reduce the number of calls 99 * to {@link #findBestCandidateView(float, float, boolean)}. 100 */ 101 @Nullable 102 private WeakReference<View> mCachedHoverTarget = null; 103 104 /** 105 * Whether to show the hover icon for the current connected view. 106 * Hover icon should be hidden for the current connected view after handwriting is initiated 107 * for it until one of the following events happens: 108 * a) user performs a click or long click. In other words, if it receives a series of motion 109 * events that don't trigger handwriting, show hover icon again. 110 * b) the stylus hovers on another editor that supports handwriting (or a handwriting delegate). 111 * c) the current connected editor lost focus. 112 * 113 * If the stylus is hovering on an unconnected editor that supports handwriting, we always show 114 * the hover icon. 115 */ 116 private boolean mShowHoverIconForConnectedView = true; 117 118 @VisibleForTesting HandwritingInitiator(@onNull ViewConfiguration viewConfiguration, @NonNull InputMethodManager inputMethodManager)119 public HandwritingInitiator(@NonNull ViewConfiguration viewConfiguration, 120 @NonNull InputMethodManager inputMethodManager) { 121 mHandwritingSlop = viewConfiguration.getScaledHandwritingSlop(); 122 mHandwritingTimeoutInMillis = ViewConfiguration.getLongPressTimeout(); 123 mImm = inputMethodManager; 124 } 125 126 /** 127 * Notify the HandwritingInitiator that a new MotionEvent has arrived. 128 * 129 * <p>The return value indicates whether the event has been fully handled by the 130 * HandwritingInitiator and should not be dispatched to the view tree. This will be true for 131 * ACTION_MOVE events from a stylus gesture after handwriting mode has been initiated, in order 132 * to suppress other actions such as scrolling. 133 * 134 * <p>If HandwritingInitiator triggers the handwriting mode, a fabricated ACTION_CANCEL event 135 * will be sent to the ViewRootImpl. 136 * 137 * @param motionEvent the stylus {@link MotionEvent} 138 * @return true if the event has been fully handled by the {@link HandwritingInitiator} and 139 * should not be dispatched to the {@link View} tree, or false if the event should be dispatched 140 * to the {@link View} tree as usual 141 */ 142 @VisibleForTesting onTouchEvent(@onNull MotionEvent motionEvent)143 public boolean onTouchEvent(@NonNull MotionEvent motionEvent) { 144 final int maskedAction = motionEvent.getActionMasked(); 145 switch (maskedAction) { 146 case MotionEvent.ACTION_DOWN: 147 case MotionEvent.ACTION_POINTER_DOWN: 148 mState = null; 149 final int actionIndex = motionEvent.getActionIndex(); 150 final int toolType = motionEvent.getToolType(actionIndex); 151 // TOOL_TYPE_ERASER is also from stylus. This indicates that the user is holding 152 // the eraser button during handwriting. 153 if (toolType != MotionEvent.TOOL_TYPE_STYLUS 154 && toolType != MotionEvent.TOOL_TYPE_ERASER) { 155 // The motion event is not from a stylus event, ignore it. 156 return false; 157 } 158 mState = new State(motionEvent); 159 break; 160 case MotionEvent.ACTION_POINTER_UP: 161 final int pointerId = motionEvent.getPointerId(motionEvent.getActionIndex()); 162 if (mState == null || pointerId != mState.mStylusPointerId) { 163 // ACTION_POINTER_UP is from another stylus pointer, ignore the event. 164 return false; 165 } 166 // Deliberately fall through. 167 case MotionEvent.ACTION_CANCEL: 168 case MotionEvent.ACTION_UP: 169 // If it's ACTION_CANCEL or ACTION_UP, all the pointers go up. There is no need to 170 // check whether the stylus we are tracking goes up. 171 if (mState != null) { 172 mState.mShouldInitHandwriting = false; 173 if (!mState.mHasInitiatedHandwriting 174 && !mState.mHasPreparedHandwritingDelegation) { 175 // The user just did a click, long click or another stylus gesture, 176 // show hover icon again for the connected view. 177 mShowHoverIconForConnectedView = true; 178 } 179 } 180 return false; 181 case MotionEvent.ACTION_MOVE: 182 if (mState == null) { 183 return false; 184 } 185 186 // Either we've already tried to initiate handwriting, or the ongoing MotionEvent 187 // sequence is considered to be tap, long-click or other gestures. 188 if (!mState.mShouldInitHandwriting || mState.mExceedHandwritingSlop) { 189 return mState.mHasInitiatedHandwriting 190 || mState.mHasPreparedHandwritingDelegation; 191 } 192 193 final long timeElapsed = 194 motionEvent.getEventTime() - mState.mStylusDownTimeInMillis; 195 if (timeElapsed > mHandwritingTimeoutInMillis) { 196 mState.mShouldInitHandwriting = false; 197 return mState.mHasInitiatedHandwriting 198 || mState.mHasPreparedHandwritingDelegation; 199 } 200 201 final int pointerIndex = motionEvent.findPointerIndex(mState.mStylusPointerId); 202 final float x = motionEvent.getX(pointerIndex); 203 final float y = motionEvent.getY(pointerIndex); 204 if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) { 205 mState.mExceedHandwritingSlop = true; 206 View candidateView = findBestCandidateView(mState.mStylusDownX, 207 mState.mStylusDownY, /* isHover */ false); 208 if (candidateView != null) { 209 if (candidateView == getConnectedView()) { 210 if (!candidateView.hasFocus()) { 211 requestFocusWithoutReveal(candidateView); 212 } 213 startHandwriting(candidateView); 214 } else if (candidateView.getHandwritingDelegatorCallback() != null) { 215 String delegatePackageName = 216 candidateView.getAllowedHandwritingDelegatePackageName(); 217 if (delegatePackageName == null) { 218 delegatePackageName = candidateView.getContext().getOpPackageName(); 219 } 220 mImm.prepareStylusHandwritingDelegation( 221 candidateView, delegatePackageName); 222 candidateView.getHandwritingDelegatorCallback().run(); 223 mState.mHasPreparedHandwritingDelegation = true; 224 } else { 225 mState.mPendingConnectedView = new WeakReference<>(candidateView); 226 requestFocusWithoutReveal(candidateView); 227 } 228 } 229 } 230 return mState.mHasInitiatedHandwriting || mState.mHasPreparedHandwritingDelegation; 231 } 232 return false; 233 } 234 235 @Nullable getConnectedView()236 private View getConnectedView() { 237 if (mConnectedView == null) return null; 238 return mConnectedView.get(); 239 } 240 clearConnectedView()241 private void clearConnectedView() { 242 mConnectedView = null; 243 mConnectionCount = 0; 244 } 245 246 /** 247 * Notify HandwritingInitiator that a delegate view (see {@link View#isHandwritingDelegate}) 248 * gained focus. 249 */ onDelegateViewFocused(@onNull View view)250 public void onDelegateViewFocused(@NonNull View view) { 251 if (view == getConnectedView()) { 252 if (tryAcceptStylusHandwritingDelegation(view)) { 253 // A handwriting delegate view is accepted and handwriting starts; hide the 254 // hover icon. 255 mShowHoverIconForConnectedView = false; 256 } 257 } 258 } 259 260 /** 261 * Notify HandwritingInitiator that a new InputConnection is created. 262 * The caller of this method should guarantee that each onInputConnectionCreated call 263 * is paired with a onInputConnectionClosed call. 264 * @param view the view that created the current InputConnection. 265 * @see #onInputConnectionClosed(View) 266 */ onInputConnectionCreated(@onNull View view)267 public void onInputConnectionCreated(@NonNull View view) { 268 if (!view.isAutoHandwritingEnabled()) { 269 clearConnectedView(); 270 return; 271 } 272 273 final View connectedView = getConnectedView(); 274 if (connectedView == view) { 275 ++mConnectionCount; 276 } else { 277 mConnectedView = new WeakReference<>(view); 278 mConnectionCount = 1; 279 // A new view just gain focus. By default, we should show hover icon for it. 280 mShowHoverIconForConnectedView = true; 281 if (view.isHandwritingDelegate() && tryAcceptStylusHandwritingDelegation(view)) { 282 // A handwriting delegate view is accepted and handwriting starts; hide the 283 // hover icon. 284 mShowHoverIconForConnectedView = false; 285 return; 286 } 287 if (mState != null && mState.mPendingConnectedView != null 288 && mState.mPendingConnectedView.get() == view) { 289 startHandwriting(view); 290 } 291 } 292 } 293 294 /** 295 * Notify HandwritingInitiator that the InputConnection has closed for the given view. 296 * The caller of this method should guarantee that each onInputConnectionClosed call 297 * is paired with a onInputConnectionCreated call. 298 * @param view the view that closed the InputConnection. 299 */ onInputConnectionClosed(@onNull View view)300 public void onInputConnectionClosed(@NonNull View view) { 301 final View connectedView = getConnectedView(); 302 if (connectedView == null) return; 303 if (connectedView == view) { 304 --mConnectionCount; 305 if (mConnectionCount == 0) { 306 clearConnectedView(); 307 } 308 } else { 309 // Unexpected branch, set mConnectedView to null to avoid further problem. 310 clearConnectedView(); 311 } 312 } 313 314 /** Starts a stylus handwriting session for the view. */ 315 @VisibleForTesting startHandwriting(@onNull View view)316 public void startHandwriting(@NonNull View view) { 317 mImm.startStylusHandwriting(view); 318 mState.mHasInitiatedHandwriting = true; 319 mState.mShouldInitHandwriting = false; 320 mShowHoverIconForConnectedView = false; 321 if (view instanceof TextView) { 322 ((TextView) view).hideHint(); 323 } 324 } 325 326 /** 327 * Starts a stylus handwriting session for the delegate view, if {@link 328 * InputMethodManager#prepareStylusHandwritingDelegation} was previously called. 329 */ 330 @VisibleForTesting tryAcceptStylusHandwritingDelegation(@onNull View view)331 public boolean tryAcceptStylusHandwritingDelegation(@NonNull View view) { 332 String delegatorPackageName = 333 view.getAllowedHandwritingDelegatorPackageName(); 334 if (delegatorPackageName == null) { 335 delegatorPackageName = view.getContext().getOpPackageName(); 336 } 337 if (mImm.acceptStylusHandwritingDelegation(view, delegatorPackageName)) { 338 if (mState != null) { 339 mState.mHasInitiatedHandwriting = true; 340 mState.mShouldInitHandwriting = false; 341 } 342 if (view instanceof TextView) { 343 ((TextView) view).hideHint(); 344 } 345 return true; 346 } 347 return false; 348 } 349 350 /** 351 * Notify that the handwriting area for the given view might be updated. 352 * @param view the view whose handwriting area might be updated. 353 */ updateHandwritingAreasForView(@onNull View view)354 public void updateHandwritingAreasForView(@NonNull View view) { 355 mHandwritingAreasTracker.updateHandwritingAreaForView(view); 356 } 357 shouldTriggerStylusHandwritingForView(@onNull View view)358 private static boolean shouldTriggerStylusHandwritingForView(@NonNull View view) { 359 if (!view.shouldInitiateHandwriting()) { 360 return false; 361 } 362 // The view may be a handwriting initiation delegator, in which case it is not the editor 363 // view for which handwriting would be started. However, in almost all cases, the return 364 // values of View#isStylusHandwritingAvailable will be the same for the delegator view and 365 // the delegate editor view. So the delegator view can be used to decide whether handwriting 366 // should be triggered. 367 return view.isStylusHandwritingAvailable(); 368 } 369 370 /** 371 * Returns the pointer icon for the motion event, or null if it doesn't specify the icon. 372 * This gives HandwritingInitiator a chance to show the stylus handwriting icon over a 373 * handwrite-able area. 374 */ onResolvePointerIcon(Context context, MotionEvent event)375 public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) { 376 final View hoverView = findHoverView(event); 377 if (hoverView == null) { 378 return null; 379 } 380 381 if (mShowHoverIconForConnectedView) { 382 return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING); 383 } 384 385 if (hoverView != getConnectedView()) { 386 // The stylus is hovering on another view that supports handwriting. We should show 387 // hover icon. Also reset the mShowHoverIconForConnectedView so that hover 388 // icon is displayed again next time when the stylus hovers on connected view. 389 mShowHoverIconForConnectedView = true; 390 return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING); 391 } 392 return null; 393 } 394 getCachedHoverTarget()395 private View getCachedHoverTarget() { 396 if (mCachedHoverTarget == null) { 397 return null; 398 } 399 return mCachedHoverTarget.get(); 400 } 401 findHoverView(MotionEvent event)402 private View findHoverView(MotionEvent event) { 403 if (!event.isStylusPointer() || !event.isHoverEvent()) { 404 return null; 405 } 406 407 if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER 408 || event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) { 409 final float hoverX = event.getX(event.getActionIndex()); 410 final float hoverY = event.getY(event.getActionIndex()); 411 412 final View cachedHoverTarget = getCachedHoverTarget(); 413 if (cachedHoverTarget != null) { 414 final Rect handwritingArea = mTempRect; 415 if (getViewHandwritingArea(cachedHoverTarget, handwritingArea) 416 && isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget, 417 /* isHover */ true) 418 && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) { 419 return cachedHoverTarget; 420 } 421 } 422 423 final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true); 424 425 if (candidateView != null) { 426 mCachedHoverTarget = new WeakReference<>(candidateView); 427 return candidateView; 428 } 429 } 430 431 mCachedHoverTarget = null; 432 return null; 433 } 434 requestFocusWithoutReveal(View view)435 private void requestFocusWithoutReveal(View view) { 436 if (view instanceof EditText editText && !mState.mStylusDownWithinEditorBounds) { 437 // If the stylus down point was inside the EditText's bounds, then the EditText will 438 // automatically set its cursor position nearest to the stylus down point when it 439 // gains focus. If the stylus down point was outside the EditText's bounds (within 440 // the extended handwriting bounds), then we must calculate and set the cursor 441 // position manually. 442 view.getLocationInWindow(mTempLocation); 443 int offset = editText.getOffsetForPosition( 444 mState.mStylusDownX - mTempLocation[0], 445 mState.mStylusDownY - mTempLocation[1]); 446 editText.setSelection(offset); 447 } 448 if (view.getRevealOnFocusHint()) { 449 view.setRevealOnFocusHint(false); 450 view.requestFocus(); 451 view.setRevealOnFocusHint(true); 452 } else { 453 view.requestFocus(); 454 } 455 } 456 457 /** 458 * Given the location of the stylus event, return the best candidate view to initialize 459 * handwriting mode. 460 * 461 * @param x the x coordinates of the stylus event, in the coordinates of the window. 462 * @param y the y coordinates of the stylus event, in the coordinates of the window. 463 */ 464 @Nullable findBestCandidateView(float x, float y, boolean isHover)465 private View findBestCandidateView(float x, float y, boolean isHover) { 466 // If the connectedView is not null and do not set any handwriting area, it will check 467 // whether the connectedView's boundary contains the initial stylus position. If true, 468 // directly return the connectedView. 469 final View connectedView = getConnectedView(); 470 if (connectedView != null) { 471 Rect handwritingArea = mTempRect; 472 if (getViewHandwritingArea(connectedView, handwritingArea) 473 && isInHandwritingArea(handwritingArea, x, y, connectedView, isHover) 474 && shouldTriggerStylusHandwritingForView(connectedView)) { 475 if (!isHover && mState != null) { 476 mState.mStylusDownWithinEditorBounds = 477 contains(handwritingArea, x, y, 0f, 0f, 0f, 0f); 478 } 479 return connectedView; 480 } 481 } 482 483 float minDistance = Float.MAX_VALUE; 484 View bestCandidate = null; 485 // Check the registered handwriting areas. 486 final List<HandwritableViewInfo> handwritableViewInfos = 487 mHandwritingAreasTracker.computeViewInfos(); 488 for (HandwritableViewInfo viewInfo : handwritableViewInfos) { 489 final View view = viewInfo.getView(); 490 final Rect handwritingArea = viewInfo.getHandwritingArea(); 491 if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) 492 || !shouldTriggerStylusHandwritingForView(view)) { 493 continue; 494 } 495 496 final float distance = distance(handwritingArea, x, y); 497 if (distance == 0f) { 498 if (!isHover && mState != null) { 499 mState.mStylusDownWithinEditorBounds = true; 500 } 501 return view; 502 } 503 if (distance < minDistance) { 504 minDistance = distance; 505 bestCandidate = view; 506 } 507 } 508 return bestCandidate; 509 } 510 511 /** 512 * Return the square of the distance from point (x, y) to the given rect, which is mainly used 513 * for comparison. The distance is defined to be: the shortest distance between (x, y) to any 514 * point on rect. When (x, y) is contained by the rect, return 0f. 515 */ distance(@onNull Rect rect, float x, float y)516 private static float distance(@NonNull Rect rect, float x, float y) { 517 if (contains(rect, x, y, 0f, 0f, 0f, 0f)) { 518 return 0f; 519 } 520 521 /* The distance between point (x, y) and rect, there are 2 basic cases: 522 * a) The distance is the distance from (x, y) to the closest corner on rect. 523 * o | | 524 * ---+-----+--- 525 * | | 526 * ---+-----+--- 527 * | | 528 * b) The distance is the distance from (x, y) to the closest edge on rect. 529 * | o | 530 * ---+-----+--- 531 * | | 532 * ---+-----+--- 533 * | | 534 * We define xDistance as following(similar for yDistance): 535 * If x is in [left, right) 0, else min(abs(x - left), abs(x - y)) 536 * For case a, sqrt(xDistance^2 + yDistance^2) is the final distance. 537 * For case b, distance should be yDistance, which is also equal to 538 * sqrt(xDistance^2 + yDistance^2) because xDistance is 0. 539 */ 540 final float xDistance; 541 if (x >= rect.left && x < rect.right) { 542 xDistance = 0f; 543 } else if (x < rect.left) { 544 xDistance = rect.left - x; 545 } else { 546 xDistance = x - rect.right; 547 } 548 549 final float yDistance; 550 if (y >= rect.top && y < rect.bottom) { 551 yDistance = 0f; 552 } else if (y < rect.top) { 553 yDistance = rect.top - y; 554 } else { 555 yDistance = y - rect.bottom; 556 } 557 // We can omit sqrt here because we only need the distance for comparison. 558 return xDistance * xDistance + yDistance * yDistance; 559 } 560 561 /** 562 * Return the handwriting area of the given view, represented in the window's coordinate. 563 * If the view didn't set any handwriting area, it will return the view's boundary. 564 * 565 * <p> The handwriting area is clipped to its visible part. 566 * Notice that the returned rectangle is the view's original handwriting area without the 567 * view's handwriting area extends. </p> 568 * 569 * @param view the {@link View} whose handwriting area we want to compute. 570 * @param rect the {@link Rect} to receive the result. 571 * 572 * @return true if the view's handwriting area is still visible, or false if it's clipped and 573 * fully invisible. This method only consider the clip by given view's parents, but not the case 574 * where a view is covered by its sibling view. 575 */ getViewHandwritingArea(@onNull View view, @NonNull Rect rect)576 private static boolean getViewHandwritingArea(@NonNull View view, @NonNull Rect rect) { 577 final ViewParent viewParent = view.getParent(); 578 if (viewParent != null && view.isAttachedToWindow() && view.isAggregatedVisible()) { 579 final Rect localHandwritingArea = view.getHandwritingArea(); 580 if (localHandwritingArea != null) { 581 rect.set(localHandwritingArea); 582 } else { 583 rect.set(0, 0, view.getWidth(), view.getHeight()); 584 } 585 return viewParent.getChildVisibleRect(view, rect, null); 586 } 587 return false; 588 } 589 590 /** 591 * Return true if the (x, y) is inside by the given {@link Rect} with the View's 592 * handwriting bounds with offsets applied. 593 */ isInHandwritingArea(@ullable Rect handwritingArea, float x, float y, View view, boolean isHover)594 private boolean isInHandwritingArea(@Nullable Rect handwritingArea, 595 float x, float y, View view, boolean isHover) { 596 if (handwritingArea == null) return false; 597 598 if (!contains(handwritingArea, x, y, 599 view.getHandwritingBoundsOffsetLeft(), 600 view.getHandwritingBoundsOffsetTop(), 601 view.getHandwritingBoundsOffsetRight(), 602 view.getHandwritingBoundsOffsetBottom())) { 603 return false; 604 } 605 606 // The returned handwritingArea computed by ViewParent#getChildVisibleRect didn't consider 607 // the case where a view is stacking on top of the editor. (e.g. DrawerLayout, popup) 608 // We must check the hit region of the editor again, and avoid the case where another 609 // view on top of the editor is handling MotionEvents. 610 ViewParent parent = view.getParent(); 611 if (parent == null) { 612 return true; 613 } 614 615 Region region = mTempRegion; 616 mTempRegion.set(0, 0, view.getWidth(), view.getHeight()); 617 Matrix matrix = mTempMatrix; 618 matrix.reset(); 619 if (!parent.getChildLocalHitRegion(view, region, matrix, isHover)) { 620 return false; 621 } 622 623 // It's not easy to extend the region by the given handwritingBoundsOffset. Instead, we 624 // create a rectangle surrounding the motion event location and check if this rectangle 625 // overlaps with the hit region of the editor. 626 float left = x - view.getHandwritingBoundsOffsetRight(); 627 float top = y - view.getHandwritingBoundsOffsetBottom(); 628 float right = Math.max(x + view.getHandwritingBoundsOffsetLeft(), left + 1); 629 float bottom = Math.max(y + view.getHandwritingBoundsOffsetTop(), top + 1); 630 RectF rectF = mTempRectF; 631 rectF.set(left, top, right, bottom); 632 matrix.mapRect(rectF); 633 634 return region.op(Math.round(rectF.left), Math.round(rectF.top), 635 Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT); 636 } 637 638 /** 639 * Return true if the (x, y) is inside by the given {@link Rect} offset by the given 640 * offsetLeft, offsetTop, offsetRight and offsetBottom. 641 */ contains(@onNull Rect rect, float x, float y, float offsetLeft, float offsetTop, float offsetRight, float offsetBottom)642 private static boolean contains(@NonNull Rect rect, float x, float y, 643 float offsetLeft, float offsetTop, float offsetRight, float offsetBottom) { 644 return x >= rect.left - offsetLeft && x < rect.right + offsetRight 645 && y >= rect.top - offsetTop && y < rect.bottom + offsetBottom; 646 } 647 largerThanTouchSlop(float x1, float y1, float x2, float y2)648 private boolean largerThanTouchSlop(float x1, float y1, float x2, float y2) { 649 float dx = x1 - x2; 650 float dy = y1 - y2; 651 return dx * dx + dy * dy > mHandwritingSlop * mHandwritingSlop; 652 } 653 654 /** Object that keeps the MotionEvent related states for HandwritingInitiator. */ 655 private static class State { 656 /** 657 * Whether it should initiate handwriting mode for the current MotionEvent sequence. 658 * (A series of MotionEvents from ACTION_DOWN to ACTION_UP) 659 * 660 * The purpose of this boolean value is: 661 * a) We should only request to start handwriting mode ONCE for each MotionEvent sequence. 662 * If we've already requested to enter handwriting mode for the ongoing MotionEvent 663 * sequence, this boolean is set to false. And it won't request to start handwriting again. 664 * 665 * b) If the MotionEvent sequence is considered to be tap, long-click or other gestures. 666 * This boolean will be set to false, and it won't request to start handwriting. 667 */ 668 private boolean mShouldInitHandwriting; 669 /** 670 * Whether handwriting mode has already been initiated for the current MotionEvent sequence. 671 */ 672 private boolean mHasInitiatedHandwriting; 673 674 private boolean mHasPreparedHandwritingDelegation; 675 676 /** 677 * Whether the current ongoing stylus MotionEvent sequence already exceeds the 678 * handwriting slop. 679 * It's used for the case where the stylus exceeds handwriting slop before the target View 680 * built InputConnection. 681 */ 682 private boolean mExceedHandwritingSlop; 683 684 /** 685 * Whether the stylus down point of the MotionEvent sequence was within the editor's bounds 686 * (not including the extended handwriting bounds). 687 */ 688 private boolean mStylusDownWithinEditorBounds; 689 690 /** 691 * A view which has requested focus and is pending input connection creation. When an input 692 * connection is created for the view, a handwriting session should be started for the view. 693 */ 694 private WeakReference<View> mPendingConnectedView = null; 695 696 /** The pointer id of the stylus pointer that is being tracked. */ 697 private final int mStylusPointerId; 698 /** The time stamp when the stylus pointer goes down. */ 699 private final long mStylusDownTimeInMillis; 700 /** The initial location where the stylus pointer goes down. */ 701 private final float mStylusDownX; 702 private final float mStylusDownY; 703 State(MotionEvent motionEvent)704 private State(MotionEvent motionEvent) { 705 final int actionIndex = motionEvent.getActionIndex(); 706 mStylusPointerId = motionEvent.getPointerId(actionIndex); 707 mStylusDownTimeInMillis = motionEvent.getEventTime(); 708 mStylusDownX = motionEvent.getX(actionIndex); 709 mStylusDownY = motionEvent.getY(actionIndex); 710 711 mShouldInitHandwriting = true; 712 mHasInitiatedHandwriting = false; 713 mHasPreparedHandwritingDelegation = false; 714 mExceedHandwritingSlop = false; 715 } 716 } 717 718 /** The helper method to check if the given view is still active for handwriting. */ isViewActive(@ullable View view)719 private static boolean isViewActive(@Nullable View view) { 720 return view != null && view.isAttachedToWindow() && view.isAggregatedVisible() 721 && view.shouldInitiateHandwriting(); 722 } 723 724 /** 725 * A class used to track the handwriting areas set by the Views. 726 * 727 * @hide 728 */ 729 @VisibleForTesting 730 public static class HandwritingAreaTracker { 731 private final List<HandwritableViewInfo> mHandwritableViewInfos; 732 HandwritingAreaTracker()733 public HandwritingAreaTracker() { 734 mHandwritableViewInfos = new ArrayList<>(); 735 } 736 737 /** 738 * Notify this tracker that the handwriting area of the given view has been updated. 739 * This method does three things: 740 * a) iterate over the all the tracked ViewInfos and remove those already invalid ones. 741 * b) mark the given view's ViewInfo to be dirty. So that next time when 742 * {@link #computeViewInfos} is called, this view's handwriting area will be recomputed. 743 * c) If no the given view is not in the tracked ViewInfo list, a new ViewInfo object will 744 * be created and added to the list. 745 * 746 * @param view the view whose handwriting area is updated. 747 */ updateHandwritingAreaForView(@onNull View view)748 public void updateHandwritingAreaForView(@NonNull View view) { 749 Iterator<HandwritableViewInfo> iterator = mHandwritableViewInfos.iterator(); 750 boolean found = false; 751 while (iterator.hasNext()) { 752 final HandwritableViewInfo handwritableViewInfo = iterator.next(); 753 final View curView = handwritableViewInfo.getView(); 754 if (!isViewActive(curView)) { 755 iterator.remove(); 756 } 757 if (curView == view) { 758 found = true; 759 handwritableViewInfo.mIsDirty = true; 760 } 761 } 762 if (!found && isViewActive(view)) { 763 // The given view is not tracked. Create a new HandwritableViewInfo for it and add 764 // to the list. 765 mHandwritableViewInfos.add(new HandwritableViewInfo(view)); 766 } 767 } 768 769 /** 770 * Update the handwriting areas and return a list of ViewInfos containing the view 771 * reference and its handwriting area. 772 */ 773 @NonNull computeViewInfos()774 public List<HandwritableViewInfo> computeViewInfos() { 775 mHandwritableViewInfos.removeIf(viewInfo -> !viewInfo.update()); 776 return mHandwritableViewInfos; 777 } 778 } 779 780 /** 781 * A class that reference to a View and its handwriting area(in the ViewRoot's coordinate.) 782 * 783 * @hide 784 */ 785 @VisibleForTesting 786 public static class HandwritableViewInfo { 787 final WeakReference<View> mViewRef; 788 Rect mHandwritingArea = null; 789 @VisibleForTesting 790 public boolean mIsDirty = true; 791 792 @VisibleForTesting HandwritableViewInfo(@onNull View view)793 public HandwritableViewInfo(@NonNull View view) { 794 mViewRef = new WeakReference<>(view); 795 } 796 797 /** Return the tracked view. */ 798 @Nullable getView()799 public View getView() { 800 return mViewRef.get(); 801 } 802 803 /** 804 * Return the tracked handwriting area, represented in the ViewRoot's coordinates. 805 * Notice, the caller should not modify the returned Rect. 806 */ 807 @Nullable getHandwritingArea()808 public Rect getHandwritingArea() { 809 return mHandwritingArea; 810 } 811 812 /** 813 * Update the handwriting area in this ViewInfo. 814 * 815 * @return true if this ViewInfo is still valid. Or false if this ViewInfo has become 816 * invalid due to either view is no longer visible, or the handwriting area set by the 817 * view is removed. {@link HandwritingAreaTracker} no longer need to keep track of this 818 * HandwritableViewInfo this method returns false. 819 */ update()820 public boolean update() { 821 final View view = getView(); 822 if (!isViewActive(view)) { 823 return false; 824 } 825 826 if (!mIsDirty) { 827 return true; 828 } 829 final Rect handwritingArea = view.getHandwritingArea(); 830 if (handwritingArea == null) { 831 return false; 832 } 833 834 ViewParent parent = view.getParent(); 835 if (parent != null) { 836 if (mHandwritingArea == null) { 837 mHandwritingArea = new Rect(); 838 } 839 mHandwritingArea.set(handwritingArea); 840 if (!parent.getChildVisibleRect(view, mHandwritingArea, null /* offset */)) { 841 mHandwritingArea = null; 842 } 843 } 844 mIsDirty = false; 845 return true; 846 } 847 } 848 } 849