Loading core/java/android/view/ViewGroup.java +48 −12 Original line number Diff line number Diff line Loading @@ -7397,19 +7397,26 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } target = next; } if (!childIsHit) { if (!childIsHit && mFirstHoverTarget != null) { target = mFirstHoverTarget; final ArrayList<View> preorderedList = buildTouchDispatchChildList(); while (notEmpty && target != null) { final HoverTarget next = target.next; final View hoveredView = target.child; if (!isOnTop(child, hoveredView, preorderedList)) { 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); Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); } target = next; } if (preorderedList != null) { preorderedList.clear(); } } } else { TouchTarget target = mFirstTouchTarget; Loading @@ -7422,19 +7429,26 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } target = next; } if (!childIsHit) { if (!childIsHit && mFirstTouchTarget != null) { target = mFirstTouchTarget; final ArrayList<View> preorderedList = buildOrderedChildList(); while (notEmpty && target != null) { final TouchTarget next = target.next; final View touchedView = target.child; if (!isOnTop(child, touchedView, preorderedList)) { 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); Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); } target = next; } if (preorderedList != null) { preorderedList.clear(); } } } Loading @@ -7444,6 +7458,28 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return notEmpty; } /** * Return true if the given {@code view} is drawn on top of the {@code otherView}. * Both the {@code view} and {@code otherView} must be children of this ViewGroup. * Otherwise, the returned value is meaningless. */ private boolean isOnTop(View view, View otherView, ArrayList<View> preorderedList) { final int childrenCount = mChildrenCount; final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if (child == view) { return true; } if (child == otherView) { return false; } } // Can't find the view and otherView in the children list. Return value is meaningless. return false; } private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) { final int[] locationInWindow = new int[2]; Loading core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java +55 −4 Original line number Diff line number Diff line Loading @@ -90,22 +90,73 @@ public class ViewGroupGetChildLocalHitRegionTest { assertGetChildLocalHitRegionEmpty(R.id.view_cover_top, R.id.view_cover_bottom); } @Test public void testGetChildLocalHitRegion_topViewIsNotBlockedByBottomView() { // In this case, two views overlap with each other and the MotionEvent is injected to the // bottom view. It verifies that the hit region of the top view won't be blocked by the // bottom view. testGetChildLocalHitRegion_topViewIsNotBlockedByBottomView(/* isHover= */ true); testGetChildLocalHitRegion_topViewIsNotBlockedByBottomView(/* isHover= */ false); } private void testGetChildLocalHitRegion_topViewIsNotBlockedByBottomView(boolean isHover) { // In this case, two views overlap with each other and the MotionEvent is injected to the // bottom view. It verifies that the hit region of the top view won't be blocked by the // bottom view. mScenarioRule.getScenario().onActivity(activity -> { View viewTop = activity.findViewById(R.id.view_overlap_top); View viewBottom = activity.findViewById(R.id.view_overlap_bottom); // The viewTop covers the left side of the viewBottom. To avoid the MotionEvent gets // blocked by viewTop, we inject MotionEvents into viewBottom's right bottom corner. float x = viewBottom.getWidth() - 1; float y = viewBottom.getHeight() - 1; injectMotionEvent(viewBottom, x, y, isHover); Matrix actualMatrix = new Matrix(); Region actualRegion = new Region(0, 0, viewTop.getWidth(), viewTop.getHeight()); boolean actualNotEmpty = viewTop.getParent() .getChildLocalHitRegion(viewTop, actualRegion, actualMatrix, isHover); int[] windowLocation = new int[2]; viewTop.getLocationInWindow(windowLocation); Matrix expectMatrix = new Matrix(); expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]); // Though viewTop and viewBottom overlaps, viewTop's hit region won't be blocked by // viewBottom. Region expectRegion = new Region(0, 0, viewTop.getWidth(), viewTop.getHeight()); assertThat(actualNotEmpty).isTrue(); assertThat(actualMatrix).isEqualTo(expectMatrix); assertThat(actualRegion).isEqualTo(expectRegion); }); } private void injectMotionEvent(View view, boolean isHover) { float x = view.getWidth() / 2f; float y = view.getHeight() / 2f; injectMotionEvent(view, x, y, isHover); } /** * Inject MotionEvent into the given view, at the given location specified in the view's * coordinates. */ private void injectMotionEvent(View view, float x, float y, boolean isHover) { int[] location = new int[2]; view.getLocationInWindow(location); float x = location[0] + view.getWidth() / 2f; float y = location[1] + view.getHeight() / 2f; float globalX = location[0] + x; float globalY = location[1] + y; 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, globalX, globalY, /* 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); Loading Loading
core/java/android/view/ViewGroup.java +48 −12 Original line number Diff line number Diff line Loading @@ -7397,19 +7397,26 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } target = next; } if (!childIsHit) { if (!childIsHit && mFirstHoverTarget != null) { target = mFirstHoverTarget; final ArrayList<View> preorderedList = buildTouchDispatchChildList(); while (notEmpty && target != null) { final HoverTarget next = target.next; final View hoveredView = target.child; if (!isOnTop(child, hoveredView, preorderedList)) { 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); Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); } target = next; } if (preorderedList != null) { preorderedList.clear(); } } } else { TouchTarget target = mFirstTouchTarget; Loading @@ -7422,19 +7429,26 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } target = next; } if (!childIsHit) { if (!childIsHit && mFirstTouchTarget != null) { target = mFirstTouchTarget; final ArrayList<View> preorderedList = buildOrderedChildList(); while (notEmpty && target != null) { final TouchTarget next = target.next; final View touchedView = target.child; if (!isOnTop(child, touchedView, preorderedList)) { 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); Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); } target = next; } if (preorderedList != null) { preorderedList.clear(); } } } Loading @@ -7444,6 +7458,28 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return notEmpty; } /** * Return true if the given {@code view} is drawn on top of the {@code otherView}. * Both the {@code view} and {@code otherView} must be children of this ViewGroup. * Otherwise, the returned value is meaningless. */ private boolean isOnTop(View view, View otherView, ArrayList<View> preorderedList) { final int childrenCount = mChildrenCount; final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if (child == view) { return true; } if (child == otherView) { return false; } } // Can't find the view and otherView in the children list. Return value is meaningless. return false; } private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) { final int[] locationInWindow = new int[2]; Loading
core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java +55 −4 Original line number Diff line number Diff line Loading @@ -90,22 +90,73 @@ public class ViewGroupGetChildLocalHitRegionTest { assertGetChildLocalHitRegionEmpty(R.id.view_cover_top, R.id.view_cover_bottom); } @Test public void testGetChildLocalHitRegion_topViewIsNotBlockedByBottomView() { // In this case, two views overlap with each other and the MotionEvent is injected to the // bottom view. It verifies that the hit region of the top view won't be blocked by the // bottom view. testGetChildLocalHitRegion_topViewIsNotBlockedByBottomView(/* isHover= */ true); testGetChildLocalHitRegion_topViewIsNotBlockedByBottomView(/* isHover= */ false); } private void testGetChildLocalHitRegion_topViewIsNotBlockedByBottomView(boolean isHover) { // In this case, two views overlap with each other and the MotionEvent is injected to the // bottom view. It verifies that the hit region of the top view won't be blocked by the // bottom view. mScenarioRule.getScenario().onActivity(activity -> { View viewTop = activity.findViewById(R.id.view_overlap_top); View viewBottom = activity.findViewById(R.id.view_overlap_bottom); // The viewTop covers the left side of the viewBottom. To avoid the MotionEvent gets // blocked by viewTop, we inject MotionEvents into viewBottom's right bottom corner. float x = viewBottom.getWidth() - 1; float y = viewBottom.getHeight() - 1; injectMotionEvent(viewBottom, x, y, isHover); Matrix actualMatrix = new Matrix(); Region actualRegion = new Region(0, 0, viewTop.getWidth(), viewTop.getHeight()); boolean actualNotEmpty = viewTop.getParent() .getChildLocalHitRegion(viewTop, actualRegion, actualMatrix, isHover); int[] windowLocation = new int[2]; viewTop.getLocationInWindow(windowLocation); Matrix expectMatrix = new Matrix(); expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]); // Though viewTop and viewBottom overlaps, viewTop's hit region won't be blocked by // viewBottom. Region expectRegion = new Region(0, 0, viewTop.getWidth(), viewTop.getHeight()); assertThat(actualNotEmpty).isTrue(); assertThat(actualMatrix).isEqualTo(expectMatrix); assertThat(actualRegion).isEqualTo(expectRegion); }); } private void injectMotionEvent(View view, boolean isHover) { float x = view.getWidth() / 2f; float y = view.getHeight() / 2f; injectMotionEvent(view, x, y, isHover); } /** * Inject MotionEvent into the given view, at the given location specified in the view's * coordinates. */ private void injectMotionEvent(View view, float x, float y, boolean isHover) { int[] location = new int[2]; view.getLocationInWindow(location); float x = location[0] + view.getWidth() / 2f; float y = location[1] + view.getHeight() / 2f; float globalX = location[0] + x; float globalY = location[1] + y; 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, globalX, globalY, /* 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); Loading