Android clipChildren usage and troubleshooting

preface

ClipXX series:

Android clipChildren usage and troubleshooting
Android clipToPadding usage and troubleshooting

We know that when the boundary of the child layout is outside the parent layout, the excess part of the child layout cannot be displayed. If you want to display the excess part, you can solve this problem by setting the clipChildren property. This article will explore the use and principle of the clipChildren property.
Through this article, you will learn:

1. clipChildren usage scenario
2. How to use clipChildren
3. Why is the clipChildren setting invalid in the parent layout
4. How does the excess part of the sub layout respond to click events
5. Summary

1. clipChildren usage scenario

Let's look at the picture first:

As shown in the figure above, there are three buttons at the bottom, which are wrapped in the same parent layout. The overall layout file is as follows:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <LinearLayout
        android:background="@color/red"
        android:layout_gravity="bottom"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="200px">

        <Button
            android:id="@+id/btn1"
            android:layout_marginLeft="50px"
            android:text="button 1"
            android:layout_width="0px"
            android:layout_weight="1"
            android:background="@color/green"
            android:layout_height="match_parent">
        </Button>

        <Button
            android:id="@+id/btn2"
            android:layout_marginLeft="50px"
            android:text="button 2"
            android:layout_width="0px"
            android:layout_weight="1"
            android:background="@color/green"
            android:layout_height="match_parent">
        </Button>

        <Button
            android:id="@+id/btn3"
            android:layout_marginHorizontal="50px"
            android:text="button 3"
            android:layout_width="0px"
            android:layout_weight="1"
            android:background="@color/green"
            android:layout_height="match_parent">
        </Button>
    </LinearLayout>

</FrameLayout>

The simplified hierarchy is as follows:

According to the layout file and the above figure:

1. The three buttons are placed in a horizontal linear layout.
2. The background color of the LinearLayout is red.
3. The Button height is consistent with the parent layout height.

Now you want an effect:

Click the corresponding Button to move it up to highlight the click effect.

The effects are as follows:

However, it did not achieve the expected effect.
At this point, it's the clipChildren property's turn.

2. How to use clipChildren

clipChildren as its name implies: cut the sub layout so that it does not exceed the parent layout display. This attribute is an attribute in ViewGroup.
There are two setting methods: dynamic setting and xml setting.

Dynamic settings

#ViewGroup.java
    public void setClipChildren(boolean clipChildren) {
        boolean previousValue = (mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN;
        if (clipChildren != previousValue) {
            //The marks are different and need to be set
            //Set FLAG_CLIP_CHILDREN attribute
            setBooleanFlag(FLAG_CLIP_CHILDREN, clipChildren);
            for (int i = 0; i < mChildrenCount; ++i) {
                //Traverse the sub layout to limit the drawing boundary
                View child = getChildAt(i);
                if (child.mRenderNode != null) {
                    child.mRenderNode.setClipToBounds(clipChildren);
                }
            }
            invalidate(true);
        }
    }

xml settings

android:clipChildren="true"
android:clipChildren="false"

Default value

#ViewGroup.java
    private void initViewGroup() {
        ...
        mGroupFlags |= FLAG_CLIP_CHILDREN;
        mGroupFlags |= FLAG_CLIP_TO_PADDING;
        ...
    }

The clipChildren property value defaults to true.
Based on the above points, the clipChildren value is true by default, that is, the default clipping sub layout. Therefore, in order to achieve the above effect, add the following code under the FrameLayout layout in the above layout file:

android:clipChildren="false"

The effects are as follows:

This is exactly what you want at the beginning. Of course, with the help of the clipChildren feature, we can also animate the Button. For example, after clicking the Button, we can move it outside the ViewGroup.

3. Why is the clipChildren setting invalid in the parent layout

Most articles on the Internet will only mention the previous two points when analyzing clipChildren: usage scenarios and how to use them.
Think about a question:

Since the display of child layouts is restricted and the parent layout of the Button is LinearLayout, why not set android:clipChildren = "false" under the LinearLayout node and set it under the FrameLayout node?

Of course, it is set under the parent layout node according to the normal logic at the beginning, but it has no effect. Next, analyze why it has no effect.
To know why it doesn't work, you need to find out where the clipChildren attribute value is used. We know the three processes of customizing the View: measurement, placement and drawing. Because the display is involved, it is speculated that it was cropped during the drawing process, and we thought of the clipping of Canvas when cutting the display area.
Through the drawing process analyzed in the previous article, you can directly locate the following code (software drawing as an example):

#View.java
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        //Hardware acceleration is not turned on
        if (!drawingWithRenderNode) {
            //parentFlags is the flag of the parent layout
            //If the parent layout needs to cut the child layout, that is, clipChildren==true
            //Then you need to cut the canvas
            if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
                //Software drawing offsetForScroll==true
                if (offsetForScroll) {
                    //Crop the canvas to match the sub layout size
                    //SX and sy are scroll values. When scroll is not set, SX and sy are all 0
                    canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
                } else {
                    ...
                }
            }
            ...
        }
    }

It can be seen from this:

1. If clipchildrenttrue, the sub layout will be cut by cutting the Canvas.
2. If clipChildrenfalse, the Canvas will not be cropped.

Set at parent layout node

Layout: FrameLayout
Parent layout: LinearLayout
Sub layout: Button

When clipChildrenfalse is set in the parent layout node, because the property is not set in the FrameLayout, its child layout will still be limited, that is, the drawing range of the red part on the drawing (parent layout LinearLayout) is: Canvas = [010808001280]
At this time, even if the (parent layout LinearLayout) does not limit the sub layout (clipChildrenfalse), the display range of the sub layout (Button) is still: Canvas = [010808001280] because the canvas has been limited in the previous step.
The final effect is that the child layout cannot exceed the parent layout.

Set in the layout node

When clipChildren==false is set in the FrameLayout node, the grandfather layout will not limit its child layout (the parent layout LinearLayout in the red part), so the drawing range of the parent layout is: Canvas = [0,08001280].
When the parent layout limits the display range of the sub layout (Button), canvas performs a clip operation and takes the intersection to obtain that the drawing range of the sub layout (Button) is: Canvas = [1009803001280], and the excess part (980-800) is the extra display area.
The final effect is that the child layout can exceed the parent layout.

In a word:

To display beyond the parent layout, you only need to draw the canvas of the child layout beyond the boundary of the parent layout.

Note: as explained above by taking software drawing as an example, Grandpa layout, parent layout and child layout are the same Canvas object, but after hardware acceleration is turned on, Canvas is not the same object. Please check the previous articles for specific differences.

4. How does the excess part of the sub layout respond to click events

In the third step, we have solved how to go beyond the parent layout display, and now we have introduced a new problem:

How does the excess part of the sub layout respond to click events?

As always, since the click cannot respond, let's first look at the factors affecting the click response.
Let's start with event distribution. If the clicked coordinates fall within the target View (here is the sub layout Button), it can respond.
Now the question turns to:

Click on the event distributed to which layer?

Although the Canvas of the parent layout (LinearLayout) has changed, the coordinates of its vertices (left, top, right, bottom) have not changed, so the parent layout cannot receive click events. It can be confirmed that the click event must have been distributed to Grandpa.
The question turns to:

How to pass events of grandfather layout to parent layout?
In other words, how does the parent layout expand the click area?

This reminds us of TouchDelegate, a class that focuses on expanding the click area of the target View.
The solution has been found. Look at the code:

        //expand touch area
        llParent.post(() -> {
            Rect hitRect = new Rect();
            //Gets the currently valid clickable area of the parent layout
            llParent.getHitRect(hitRect);
            //Expand parent layout click area
            hitRect.top += translationY;
            TouchDelegate touchDelegate = new TouchDelegate(hitRect, llParent);
            llParent.setClickable(true);
            ViewParent viewParent = llParent.getParent();
            if (viewParent instanceof ViewGroup) {
                ((ViewGroup) viewParent).setClickable(true);
                //Intercept event distribution in Grandpa layout
                ((ViewGroup) viewParent).setTouchDelegate(touchDelegate);
            }
        });

The purpose of the above code is:

Expand the click area of the parent layout response, and distribute events to the parent layout in the parent layout.

However, when this code is run, the sub layout (Button) still cannot respond to clicks, so go to TouchDelegate to find the answer.
When TouchDelegate is set before layout discovery, TouchDelegate.onTouchEvent(xx) will be called to detect:

#TouchDelegate.java
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();
        boolean sendToDelegate = false;
        boolean hit = true;
        boolean handled = false;
        ...
        if (sendToDelegate) {
            if (hit) {
                //If hit, move the MotionEvent coordinates to the center of the target View
                event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);
            } else {
                ...
            }
            handled = mDelegateView.dispatchTouchEvent(event);
        }
        return handled;
    }

The root cause of the problem is found: Although the parent layout (FrameLayout) receives a click event, this coordinate is its center point, and the center point does not necessarily fall in its child layout (Button), so the Button cannot receive a click event.
Fortunately, TouchDelegate is public, so we can override TouchDelegate

#SimpleTouchDelegate.java
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();
        boolean sendToDelegate = false;
        boolean hit = true;
        boolean handled = false;
        ...
        if (sendToDelegate) {
            if (hit) {
              //Do nothing after hit
            } else {
                ...
            }
            handled = mDelegateView.dispatchTouchEvent(event);
        }
        return handled;
    }

At this time, the parent layout can receive the click event, but the problem comes again:

How the parent layout passes events to the child layout, and also distinguish three different buttons.

After the parent layout receives the click event, the call will flow to onTouchEvent(xx), so it is necessary to make an article in this method. Imagine that now the onTouchEvent(xx) method of the parent layout can get the click coordinates, so you only need to judge whether the point falls within each sub layout (Button). Of course, you can't rely solely on the four vertex coordinates of the Button. You also need to use it with View.getLocationOnScreen(xx).
Therefore, you need to rewrite onTouchEvent(xx):

public class ClipViewGroup extends LinearLayout {
    public ClipViewGroup(Context context) {
        super(context);
    }

    public ClipViewGroup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //Gets the position of the coordinates relative to the screen
        float rawX = event.getRawX();
        float rawY = event.getRawY();
        View child;
        //Check whether the coordinates fall within the corresponding sub layout
        if ((child = checkChildTouch(rawX, rawY)) != null) {
            //If yes, change the coordinate value to the sub layout center point
            event.setLocation(child.getWidth() / 2, child.getHeight() / 2);
            //Distribute events to child layouts
            return child.dispatchTouchEvent(event);
        }
        return super.onTouchEvent(event);
    }

    private View checkChildTouch(float x, float y) {
        int outLocation[] = new int[2];
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == VISIBLE) {
                //Gets the visible coordinates of the View on the screen
                child.getLocationOnScreen(outLocation);
                //Click whether the coordinate falls in the visible area of the View. If so, distribute the event to it
                boolean hit = (x >= outLocation[0] && y > outLocation[1]
                        && x <= outLocation[0] + child.getWidth() && y <= outLocation[1] + child.getHeight());
                if (hit)
                    return child;
            }
        }
        return null;
    }
}

Use ClipViewGroup to override the parent layout (LinearLayout).
Finally, see the effect:

Note: in order to more prominently represent the click area, this is to move all the child layouts up beyond the parent layout

5. Summary

Although the clipChildren attribute is relatively simple and the scope of use is relatively limited, to really understand it, you need to analyze the source code of the measurement, placement and drawing process. If you want to make an article on the click area, you also need to have a certain understanding of event distribution.
Of course, these basic knowledge have been systematically analyzed in the previous articles. If you have read the previous articles, it is easier to understand clipChildren.

This article is based on Android 10.
Complete code demonstration If it's helpful, give github a compliment

If you like it, please praise and pay attention. Your encouragement is my driving force

During continuous update, work with me step by step to learn more about Android/Java

Tags: Android

Posted on Wed, 06 Oct 2021 21:28:28 -0400 by mpb001