Loading core/java/android/view/HandwritingInitiator.java +55 −12 Original line number Diff line number Diff line Loading @@ -19,7 +19,10 @@ 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.TextView; Loading Loading @@ -78,11 +81,17 @@ public class HandwritingInitiator { private int mConnectionCount = 0; private final InputMethodManager mImm; 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)}. * to {@link #findBestCandidateView(float, float, boolean)}. */ @Nullable private WeakReference<View> mCachedHoverTarget = null; Loading Loading @@ -189,8 +198,8 @@ public class HandwritingInitiator { final float y = motionEvent.getY(pointerIndex); if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) { mState.mExceedHandwritingSlop = true; View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY, /* isHover */ false); if (candidateView != null) { if (candidateView == getConnectedView()) { if (!candidateView.hasFocus()) { Loading Loading @@ -398,13 +407,14 @@ public class HandwritingInitiator { final View cachedHoverTarget = getCachedHoverTarget(); if (cachedHoverTarget != null) { final Rect handwritingArea = getViewHandwritingArea(cachedHoverTarget); if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget) if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget, /* isHover */ true) && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) { return cachedHoverTarget; } } final View candidateView = findBestCandidateView(hoverX, hoverY); final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true); if (candidateView != null) { mCachedHoverTarget = new WeakReference<>(candidateView); Loading Loading @@ -434,14 +444,14 @@ public class HandwritingInitiator { * @param y the y coordinates of the stylus event, in the coordinates of the window. */ @Nullable private View findBestCandidateView(float x, float y) { 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 = getViewHandwritingArea(connectedView); if (isInHandwritingArea(handwritingArea, x, y, connectedView) if (isInHandwritingArea(handwritingArea, x, y, connectedView, isHover) && shouldTriggerStylusHandwritingForView(connectedView)) { return connectedView; } Loading @@ -455,7 +465,7 @@ public class HandwritingInitiator { for (HandwritableViewInfo viewInfo : handwritableViewInfos) { final View view = viewInfo.getView(); final Rect handwritingArea = viewInfo.getHandwritingArea(); if (!isInHandwritingArea(handwritingArea, x, y, view) if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) || !shouldTriggerStylusHandwritingForView(view)) { continue; } Loading Loading @@ -551,15 +561,48 @@ public class HandwritingInitiator { * Return true if the (x, y) is inside by the given {@link Rect} with the View's * handwriting bounds with offsets applied. */ private static boolean isInHandwritingArea(@Nullable Rect handwritingArea, float x, float y, View view) { private boolean isInHandwritingArea(@Nullable Rect handwritingArea, float x, float y, View view, boolean isHover) { if (handwritingArea == null) return false; return contains(handwritingArea, x, y, if (!contains(handwritingArea, x, y, view.getHandwritingBoundsOffsetLeft(), view.getHandwritingBoundsOffsetTop(), view.getHandwritingBoundsOffsetRight(), view.getHandwritingBoundsOffsetBottom()); 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); } /** Loading core/java/android/view/ViewGroup.java +84 −0 Original line number Diff line number Diff line Loading @@ -7361,6 +7361,90 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } /** * @hide */ @Override public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, @NonNull Matrix matrix, boolean isHover) { if (!child.hasIdentityMatrix()) { matrix.preConcat(child.getInverseMatrix()); } final int dx = child.mLeft - mScrollX; final int dy = child.mTop - mScrollY; matrix.preTranslate(-dx, -dy); final int width = mRight - mLeft; final int height = mBottom - mTop; // Map the bounds of this view into the region's coordinates and clip the region. final RectF rect = mAttachInfo != null ? mAttachInfo.mTmpTransformRect : new RectF(); rect.set(0, 0, width, height); matrix.mapRect(rect); boolean notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), Math.round(rect.right), Math.round(rect.bottom), Region.Op.INTERSECT); if (isHover) { HoverTarget target = mFirstHoverTarget; boolean childIsHit = false; while (target != null) { final HoverTarget next = target.next; if (target.child == child) { childIsHit = true; break; } target = next; } if (!childIsHit) { target = mFirstHoverTarget; while (notEmpty && target != null) { final HoverTarget next = target.next; final View hoveredView = target.child; rect.set(hoveredView.mLeft, hoveredView.mTop, hoveredView.mRight, hoveredView.mBottom); matrix.mapRect(rect); notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); target = next; } } } else { TouchTarget target = mFirstTouchTarget; boolean childIsHit = false; while (target != null) { final TouchTarget next = target.next; if (target.child == child) { childIsHit = true; break; } target = next; } if (!childIsHit) { target = mFirstTouchTarget; while (notEmpty && target != null) { final TouchTarget next = target.next; final View touchedView = target.child; rect.set(touchedView.mLeft, touchedView.mTop, touchedView.mRight, touchedView.mBottom); matrix.mapRect(rect); notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); target = next; } } } if (notEmpty && mParent != null) { notEmpty = mParent.getChildLocalHitRegion(this, region, matrix, isHover); } return notEmpty; } private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) { final int[] locationInWindow = new int[2]; view.getLocationInWindow(locationInWindow); Loading core/java/android/view/ViewParent.java +31 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; Loading Loading @@ -685,6 +686,36 @@ public interface ViewParent { default void subtractObscuredTouchableRegion(Region touchableRegion, View view) { } /** * Compute the region where the child can receive the {@link MotionEvent}s from the root view. * * <p> Given region where the child will accept {@link MotionEvent}s. * Modify the region to the unblocked region where the child can receive the * {@link MotionEvent}s from the view root. * </p> * * <p> The given region is always clipped by the bounds of the parent views. When there are * on-going {@link MotionEvent}s, this method also makes use of the event dispatching results to * determine whether a sibling view will also block the child's hit region. * </p> * * @param child a child View, whose hit region we want to compute. * @param region the initial hit region where the child view will handle {@link MotionEvent}s, * defined in the child coordinates. Will be overwritten to the result hit region. * @param matrix the matrix that maps the given child view's coordinates to the region * coordinates. It will be modified to a matrix that maps window coordinates to * the result region's coordinates. * @param isHover if true it will return the hover events' hit region, otherwise it will * return the touch events' hit region. * @return true if the returned region is not empty. * @hide */ default boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, @NonNull Matrix matrix, boolean isHover) { region.setEmpty(); return false; } /** * Unbuffered dispatch has been requested by a child of this view parent. * This method is called by the View hierarchy to signal ancestors that a View needs to Loading core/java/android/view/ViewRootImpl.java +17 −0 Original line number Diff line number Diff line Loading @@ -127,6 +127,7 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; Loading Loading @@ -2392,6 +2393,22 @@ public final class ViewRootImpl implements ViewParent, return r.intersect(0, 0, mWidth, mHeight); } @Override public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, @NonNull Matrix matrix, boolean isHover) { if (child != mView) { throw new IllegalArgumentException("child " + child + " is not the root view " + mView + " managed by this ViewRootImpl"); } RectF rectF = new RectF(0, 0, mWidth, mHeight); matrix.mapRect(rectF); // Note: don't apply scroll offset, because we want to know its // visibility in the virtual canvas being given to the view hierarchy. return region.op(Math.round(rectF.left), Math.round(rectF.top), Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT); } @Override public void bringChildToFront(View child) { } Loading core/tests/coretests/AndroidManifest.xml +9 −0 Original line number Diff line number Diff line Loading @@ -1749,6 +1749,15 @@ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> <activity android:name="android.view.ViewGroupTestActivity" android:label="ViewGroup Test" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" Loading Loading
core/java/android/view/HandwritingInitiator.java +55 −12 Original line number Diff line number Diff line Loading @@ -19,7 +19,10 @@ 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.TextView; Loading Loading @@ -78,11 +81,17 @@ public class HandwritingInitiator { private int mConnectionCount = 0; private final InputMethodManager mImm; 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)}. * to {@link #findBestCandidateView(float, float, boolean)}. */ @Nullable private WeakReference<View> mCachedHoverTarget = null; Loading Loading @@ -189,8 +198,8 @@ public class HandwritingInitiator { final float y = motionEvent.getY(pointerIndex); if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) { mState.mExceedHandwritingSlop = true; View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY, /* isHover */ false); if (candidateView != null) { if (candidateView == getConnectedView()) { if (!candidateView.hasFocus()) { Loading Loading @@ -398,13 +407,14 @@ public class HandwritingInitiator { final View cachedHoverTarget = getCachedHoverTarget(); if (cachedHoverTarget != null) { final Rect handwritingArea = getViewHandwritingArea(cachedHoverTarget); if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget) if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget, /* isHover */ true) && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) { return cachedHoverTarget; } } final View candidateView = findBestCandidateView(hoverX, hoverY); final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true); if (candidateView != null) { mCachedHoverTarget = new WeakReference<>(candidateView); Loading Loading @@ -434,14 +444,14 @@ public class HandwritingInitiator { * @param y the y coordinates of the stylus event, in the coordinates of the window. */ @Nullable private View findBestCandidateView(float x, float y) { 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 = getViewHandwritingArea(connectedView); if (isInHandwritingArea(handwritingArea, x, y, connectedView) if (isInHandwritingArea(handwritingArea, x, y, connectedView, isHover) && shouldTriggerStylusHandwritingForView(connectedView)) { return connectedView; } Loading @@ -455,7 +465,7 @@ public class HandwritingInitiator { for (HandwritableViewInfo viewInfo : handwritableViewInfos) { final View view = viewInfo.getView(); final Rect handwritingArea = viewInfo.getHandwritingArea(); if (!isInHandwritingArea(handwritingArea, x, y, view) if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) || !shouldTriggerStylusHandwritingForView(view)) { continue; } Loading Loading @@ -551,15 +561,48 @@ public class HandwritingInitiator { * Return true if the (x, y) is inside by the given {@link Rect} with the View's * handwriting bounds with offsets applied. */ private static boolean isInHandwritingArea(@Nullable Rect handwritingArea, float x, float y, View view) { private boolean isInHandwritingArea(@Nullable Rect handwritingArea, float x, float y, View view, boolean isHover) { if (handwritingArea == null) return false; return contains(handwritingArea, x, y, if (!contains(handwritingArea, x, y, view.getHandwritingBoundsOffsetLeft(), view.getHandwritingBoundsOffsetTop(), view.getHandwritingBoundsOffsetRight(), view.getHandwritingBoundsOffsetBottom()); 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); } /** Loading
core/java/android/view/ViewGroup.java +84 −0 Original line number Diff line number Diff line Loading @@ -7361,6 +7361,90 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } /** * @hide */ @Override public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, @NonNull Matrix matrix, boolean isHover) { if (!child.hasIdentityMatrix()) { matrix.preConcat(child.getInverseMatrix()); } final int dx = child.mLeft - mScrollX; final int dy = child.mTop - mScrollY; matrix.preTranslate(-dx, -dy); final int width = mRight - mLeft; final int height = mBottom - mTop; // Map the bounds of this view into the region's coordinates and clip the region. final RectF rect = mAttachInfo != null ? mAttachInfo.mTmpTransformRect : new RectF(); rect.set(0, 0, width, height); matrix.mapRect(rect); boolean notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), Math.round(rect.right), Math.round(rect.bottom), Region.Op.INTERSECT); if (isHover) { HoverTarget target = mFirstHoverTarget; boolean childIsHit = false; while (target != null) { final HoverTarget next = target.next; if (target.child == child) { childIsHit = true; break; } target = next; } if (!childIsHit) { target = mFirstHoverTarget; while (notEmpty && target != null) { final HoverTarget next = target.next; final View hoveredView = target.child; rect.set(hoveredView.mLeft, hoveredView.mTop, hoveredView.mRight, hoveredView.mBottom); matrix.mapRect(rect); notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); target = next; } } } else { TouchTarget target = mFirstTouchTarget; boolean childIsHit = false; while (target != null) { final TouchTarget next = target.next; if (target.child == child) { childIsHit = true; break; } target = next; } if (!childIsHit) { target = mFirstTouchTarget; while (notEmpty && target != null) { final TouchTarget next = target.next; final View touchedView = target.child; rect.set(touchedView.mLeft, touchedView.mTop, touchedView.mRight, touchedView.mBottom); matrix.mapRect(rect); notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); target = next; } } } if (notEmpty && mParent != null) { notEmpty = mParent.getChildLocalHitRegion(this, region, matrix, isHover); } return notEmpty; } private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) { final int[] locationInWindow = new int[2]; view.getLocationInWindow(locationInWindow); Loading
core/java/android/view/ViewParent.java +31 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; Loading Loading @@ -685,6 +686,36 @@ public interface ViewParent { default void subtractObscuredTouchableRegion(Region touchableRegion, View view) { } /** * Compute the region where the child can receive the {@link MotionEvent}s from the root view. * * <p> Given region where the child will accept {@link MotionEvent}s. * Modify the region to the unblocked region where the child can receive the * {@link MotionEvent}s from the view root. * </p> * * <p> The given region is always clipped by the bounds of the parent views. When there are * on-going {@link MotionEvent}s, this method also makes use of the event dispatching results to * determine whether a sibling view will also block the child's hit region. * </p> * * @param child a child View, whose hit region we want to compute. * @param region the initial hit region where the child view will handle {@link MotionEvent}s, * defined in the child coordinates. Will be overwritten to the result hit region. * @param matrix the matrix that maps the given child view's coordinates to the region * coordinates. It will be modified to a matrix that maps window coordinates to * the result region's coordinates. * @param isHover if true it will return the hover events' hit region, otherwise it will * return the touch events' hit region. * @return true if the returned region is not empty. * @hide */ default boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, @NonNull Matrix matrix, boolean isHover) { region.setEmpty(); return false; } /** * Unbuffered dispatch has been requested by a child of this view parent. * This method is called by the View hierarchy to signal ancestors that a View needs to Loading
core/java/android/view/ViewRootImpl.java +17 −0 Original line number Diff line number Diff line Loading @@ -127,6 +127,7 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; Loading Loading @@ -2392,6 +2393,22 @@ public final class ViewRootImpl implements ViewParent, return r.intersect(0, 0, mWidth, mHeight); } @Override public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, @NonNull Matrix matrix, boolean isHover) { if (child != mView) { throw new IllegalArgumentException("child " + child + " is not the root view " + mView + " managed by this ViewRootImpl"); } RectF rectF = new RectF(0, 0, mWidth, mHeight); matrix.mapRect(rectF); // Note: don't apply scroll offset, because we want to know its // visibility in the virtual canvas being given to the view hierarchy. return region.op(Math.round(rectF.left), Math.round(rectF.top), Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT); } @Override public void bringChildToFront(View child) { } Loading
core/tests/coretests/AndroidManifest.xml +9 −0 Original line number Diff line number Diff line Loading @@ -1749,6 +1749,15 @@ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> <activity android:name="android.view.ViewGroupTestActivity" android:label="ViewGroup Test" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" Loading