Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3b4c0325 authored by Haoyu Zhang's avatar Haoyu Zhang
Browse files

Check view drawing order in ViewGroup#getChildLocalHitRegion

Before this change, ViewGroup#getChildLocalHitRegion will clip a view's
hit region when its slibing view is reciving MotionEvents despite of the
view order.
This doesn't work when two views overlaps and the bottom view is handling
MotionEvents. Because the top view's hit region will get clipped by the
bottom view's bounds.
This CL adds the view order check and fixed the issue.

Bug: 299859816
Test: atest ViewGroupGetChildLocalHitRegionTest
Change-Id: I68a8ab8402229ceaefeb5fb537c998705076d9f0
parent 8eea66e7
Loading
Loading
Loading
Loading
+48 −12
Original line number Diff line number Diff line
@@ -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;
@@ -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();
                }
            }
        }

@@ -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];
+55 −4
Original line number Diff line number Diff line
@@ -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);