diff options
| -rw-r--r-- | core/java/android/view/HandwritingInitiator.java | 67 | ||||
| -rw-r--r-- | core/java/android/view/ViewGroup.java | 84 | ||||
| -rw-r--r-- | core/java/android/view/ViewParent.java | 31 | ||||
| -rw-r--r-- | core/java/android/view/ViewRootImpl.java | 17 | ||||
| -rw-r--r-- | core/tests/coretests/AndroidManifest.xml | 9 | ||||
| -rw-r--r-- | core/tests/coretests/res/layout/viewgroup_test.xml | 77 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java | 216 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/ViewGroupTestActivity.java | 31 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java | 12 |
9 files changed, 531 insertions, 13 deletions
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 751cd210b94c..6e73a3c93fd7 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -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; @@ -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; @@ -184,8 +193,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()) { @@ -393,13 +402,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); @@ -429,14 +439,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; } @@ -450,7 +460,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; } @@ -546,15 +556,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); } /** diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 1b1098d9d57a..7bdff8c5b858 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -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); diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 1020d2ef02be..54bc3484d295 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -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; @@ -686,6 +687,36 @@ public interface ViewParent { } /** + * 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 * request unbuffered dispatch. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3aa610af60b0..e6ecd6e503c1 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -128,6 +128,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; @@ -2397,6 +2398,22 @@ public final class ViewRootImpl implements ViewParent, } @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) { } diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index 31755efb88ed..a358c4f6f7e9 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -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" diff --git a/core/tests/coretests/res/layout/viewgroup_test.xml b/core/tests/coretests/res/layout/viewgroup_test.xml new file mode 100644 index 000000000000..04f4f5228b06 --- /dev/null +++ b/core/tests/coretests/res/layout/viewgroup_test.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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. + --> + +<!-- Demonstrates adding/removing views from ViewGroup. See corresponding Java code. --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/linear_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <EditText + android:id="@+id/view" + android:layout_width="20dp" + android:layout_height="10dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <EditText + android:id="@+id/view_scale" + android:layout_width="20dp" + android:layout_height="10dp" + android:scaleX="0.5" + android:scaleY="2" + android:transformPivotX="0dp" + android:transformPivotY="0dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <EditText + android:id="@+id/view_translate" + android:layout_width="20dp" + android:layout_height="10dp" + android:translationX="10dp" + android:translationY="20dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <FrameLayout + android:layout_width="20dp" + android:layout_height="10dp"> + <EditText + android:id="@+id/view_overlap_bottom" + android:layout_width="20dp" + android:layout_height="10dp" + android:text="Hello World!"/> + <Button + android:id="@+id/view_overlap_top" + android:layout_width="10dp" + android:layout_height="10dp"/> + </FrameLayout> + + <FrameLayout + android:layout_width="20dp" + android:layout_height="10dp"> + <EditText + android:id="@+id/view_cover_bottom" + android:layout_width="10dp" + android:layout_height="10dp" + android:text="Hello World!"/> + <Button + android:id="@+id/view_cover_top" + android:layout_width="10dp" + android:layout_height="10dp"/> + </FrameLayout> + +</LinearLayout> diff --git a/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java new file mode 100644 index 000000000000..60a0a2adbbbe --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2023 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.google.common.truth.Truth.assertThat; + + +import android.graphics.Matrix; +import android.graphics.Region; +import android.platform.test.annotations.Presubmit; +import android.widget.LinearLayout; + +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.filters.SmallTest; + +import com.android.frameworks.coretests.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +/** + * Test basic functions of ViewGroup. + * + * Build/Install/Run: + * atest FrameworksCoreTests:ViewGroupTest + */ +@Presubmit +@SmallTest +public class ViewGroupGetChildLocalHitRegionTest { + @Rule + public ActivityScenarioRule<ViewGroupTestActivity> mScenarioRule = + new ActivityScenarioRule<>(ViewGroupTestActivity.class); + + private LinearLayout mRoot; + private final int[] mRootLocation = new int[2]; + + @Before + public void setup() { + mScenarioRule.getScenario().onActivity(activity -> { + mRoot = activity.findViewById(R.id.linear_layout); + mRoot.getLocationInWindow(mRootLocation); + }); + } + + @Test + public void testGetChildLocalHitRegion() { + assertGetChildLocalHitRegion(R.id.view); + } + + @Test + public void testGetChildLocalHitRegion_withScale() { + assertGetChildLocalHitRegion(R.id.view_scale); + } + + @Test + public void testGetChildLocalHitRegion_withTranslate() { + assertGetChildLocalHitRegion(R.id.view_translate); + } + + @Test + public void testGetChildLocalHitRegion_overlap_noMotionEvent() { + assertGetChildLocalHitRegion(R.id.view_overlap_bottom); + } + @Test + public void testGetChildLocalHitRegion_overlap_withMotionEvent() { + // In this case, view_cover_bottom is partially covered by the view_cover_top. + // The returned region is the bounds of the bottom view subtract the bounds of the top view. + assertGetChildLocalHitRegion(R.id.view_overlap_top, R.id.view_overlap_bottom); + } + + @Test + public void testGetChildLocalHitRegion_cover_withMotionEvent() { + // In this case, view_cover_bottom is completely covered by the view_cover_top. + // The returned region is expected to be empty. + assertGetChildLocalHitRegionEmpty(R.id.view_cover_top, R.id.view_cover_bottom); + } + + private void injectMotionEvent(View view, boolean isHover) { + int[] location = new int[2]; + view.getLocationInWindow(location); + + float x = location[0] + view.getWidth() / 2f; + float y = location[1] + view.getHeight() / 2f; + + int action = isHover ? MotionEvent.ACTION_HOVER_ENTER : MotionEvent.ACTION_DOWN; + MotionEvent motionEvent = MotionEvent.obtain(/* downtime= */ 0, /* eventTime= */ 0, action, + x, y, /* pressure= */ 0, /* size= */ 0, /* metaState= */ 0, + /* xPrecision= */ 1, /* yPrecision= */ 1, /* deviceId= */0, /* edgeFlags= */0); + + View rootView = view.getRootView(); + rootView.dispatchPointerEvent(motionEvent); + } + + private void assertGetChildLocalHitRegion(int viewId) { + assertGetChildLocalHitRegion(viewId, /* isHover= */ true); + assertGetChildLocalHitRegion(viewId, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion for a single view. + * @param viewId the viewId of the tested view. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegion(int viewId, boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View view = activity.findViewById(viewId); + + Matrix actualMatrix = new Matrix(); + Region actualRegion = new Region(0, 0, view.getWidth(), view.getHeight()); + boolean actualNotEmpty = view.getParent() + .getChildLocalHitRegion(view, actualRegion, actualMatrix, isHover); + + int[] windowLocation = new int[2]; + view.getLocationInWindow(windowLocation); + Matrix expectMatrix = new Matrix(); + expectMatrix.preScale(1 / view.getScaleX(), 1 / view.getScaleY()); + expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]); + + Region expectRegion = new Region(0, 0, view.getWidth(), view.getHeight()); + + assertThat(actualNotEmpty).isTrue(); + assertThat(actualMatrix).isEqualTo(expectMatrix); + assertThat(actualRegion).isEqualTo(expectRegion); + }); + } + + private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom) { + assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ true); + assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion of a view that is covered by another view. It will + * inject {@link MotionEvent}s to the view on top first and then get the hit region of the + * bottom view. + * + * @param viewIdTop the view id of the test view on top. + * @param viewIdBottom the view id of the test view at the bottom. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom, boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View viewTop = activity.findViewById(viewIdTop); + View viewBottom = activity.findViewById(viewIdBottom); + + injectMotionEvent(viewTop, isHover); + + Matrix actualMatrix = new Matrix(); + Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + boolean actualNotEmpty = viewBottom.getParent() + .getChildLocalHitRegion(viewBottom, actualRegion, actualMatrix, isHover); + + int[] windowLocation = new int[2]; + viewBottom.getLocationInWindow(windowLocation); + Matrix expectMatrix = new Matrix(); + expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]); + + Region expectRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + expectRegion.op(0, 0, viewTop.getWidth(), viewTop.getHeight(), Region.Op.DIFFERENCE); + + assertThat(actualNotEmpty).isTrue(); + assertThat(actualMatrix).isEqualTo(expectMatrix); + assertThat(actualRegion).isEqualTo(expectRegion); + }); + } + + private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom) { + assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ true); + assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion returns an empty region for a view that is + * completely covered by another view. It will inject {@link MotionEvent}s to the view on top + * first and then get the hit region of the + * bottom view. + * + * @param viewIdTop the view id of the test view on top. + * @param viewIdBottom the view id of the test view at the bottom. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom, + boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View viewTop = activity.findViewById(viewIdTop); + View viewBottom = activity.findViewById(viewIdBottom); + + injectMotionEvent(viewTop, isHover); + + Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + boolean actualNotEmpty = viewBottom.getParent() + .getChildLocalHitRegion(viewBottom, actualRegion, new Matrix(), isHover); + + assertThat(actualNotEmpty).isFalse(); + assertThat(actualRegion.isEmpty()).isTrue(); + }); + } +} diff --git a/core/tests/coretests/src/android/view/ViewGroupTestActivity.java b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java new file mode 100644 index 000000000000..b94bda5efba4 --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 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.Nullable; +import android.app.Activity; +import android.os.Bundle; + +import com.android.frameworks.coretests.R; + +public class ViewGroupTestActivity extends Activity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.viewgroup_test); + } +} diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java index 388a996d8b1c..b4c72ca3226b 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java @@ -21,7 +21,9 @@ import static org.mockito.Mockito.when; import android.app.Instrumentation; import android.content.Context; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.Region; import android.view.View; import android.view.ViewGroup; @@ -45,7 +47,7 @@ public class HandwritingTestUtil { float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) { final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); final Context context = instrumentation.getTargetContext(); - // mock a parent so that HandwritingInitiator can get + // mock a parent so that HandwritingInitiator can get visible rect and hit region. final ViewGroup parent = new ViewGroup(context) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { @@ -56,6 +58,14 @@ public class HandwritingTestUtil { r.set(handwritingArea); return true; } + + @Override + public boolean getChildLocalHitRegion(View child, Region region, Matrix matrix, + boolean isHover) { + matrix.reset(); + region.set(handwritingArea); + return true; + } }; View view = spy(new View(context)); |