summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/view/HandwritingInitiator.java67
-rw-r--r--core/java/android/view/ViewGroup.java84
-rw-r--r--core/java/android/view/ViewParent.java31
-rw-r--r--core/java/android/view/ViewRootImpl.java17
-rw-r--r--core/tests/coretests/AndroidManifest.xml9
-rw-r--r--core/tests/coretests/res/layout/viewgroup_test.xml77
-rw-r--r--core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java216
-rw-r--r--core/tests/coretests/src/android/view/ViewGroupTestActivity.java31
-rw-r--r--core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java12
9 files changed, 531 insertions, 13 deletions
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 4cfec99dff01..9b340079313f 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;
@@ -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()) {
@@ -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);
@@ -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;
}
@@ -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;
}
@@ -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);
}
/**
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 6edf0e24d5ab..c1ce5e0693b7 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -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;
@@ -2393,6 +2394,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));