background

Android provides a TouchDelegate class to make it easier to control the touch range of a child View, including expanding the click area of a child View and narrowing the click area of a child View. It’s easy to use. Let’s take an example.

use

  • 1. Get the superview and publish the Runnable on the interface thread. This ensures that the superview lays out its children first;
  • 2. Get an instance of the subview and call itgetHitRect()Method to obtain the boundary of its touchable region;
  • 3. Extend clickable boundaries for child Views;
  • 4. The instantiationTouchDelegate, passing in the extended rectangle and the child View to be extended;
  • 5. Instantiate the file in 4TouchDelegateSet to the parent View.

The following code increases the clickable range of an ImageButton of size 10dp x 10dp by 100px.

<? xml version="1.0" encoding="utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:background="@android:color/darker_gray"
    android:orientation="vertical">

    <ImageButton
        android:id="@+id/child_btn"
        android:layout_width="10dp"
        android:layout_height="10dp"
        android:background="@color/colorPrimaryDark"/>
</LinearLayout>

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); Runnable View parentView = findViewById(R.I.D.parent); // 1. parentView.post(newRunnable() {
            @Override
            public void run() {// 2. Get the instance of the subview and call its getHitRect() method to get the boundary of its touchable area Rect delegateRect = new Rect(); ImageButton child = findViewById(R.id.child_btn); child.setEnabled(true);
                child.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(MainActivity.this, "child clicked!", Toast.LENGTH_SHORT).show(); }}); child.getHitRect(delegateRect); // 3. Delegaterect. left -= 100; delegateRect.top -= 100; delegateRect.right += 100; delegateRect.bottom += 100; // 4. Instantiate the TouchDelegate, TouchDelegate = New TouchDelegate(delegateRect, child); // 5. Set the TouchDelegate instantiated in 4 to the parent Viewif(View.class.isInstance(child.getParent())){ ((View)child.getParent()).setTouchDelegate(touchDelegate); }}}); }}Copy the code

Very simple to use, there is nothing to say, let’s see how it is implemented.

Source code analysis

Internal implementation of the TouchDelegate

Let’s take a look at the TouchDelegate class. It’s a very simple class, and it basically does it in its onTouchEvent, and it determines whether the click event falls within the extended scope specified by the subview, and if it does, it passes the event to the mDelegateView, which is the subview, and that’s it.

    public TouchDelegate(Rect bounds, View delegateView) {
        mBounds = bounds;
        mDelegateView = delegateView;
    }

    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;

        switch (event.getActionMasked()) {
            caseMotionevent. ACTION_DOWN: // Directly determine whether the click event falls within the extension specified by the subview mDelegateTargeted = Mbounde.contains (x, y); sendToDelegate = mDelegateTargeted;break;
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_UP:
            caseMotionEvent.ACTION_MOVE: sendToDelegate = mDelegateTargeted; .break;
            case MotionEvent.ACTION_CANCEL:
                sendToDelegate = mDelegateTargeted;
                mDelegateTargeted = false;
                break;
        }
        if(sendToDelegate) { ...... / / if click on the range of fall in the View of agent area, then pass the event to mDelegateView processing, namely child View handled. = mDelegateView dispatchTouchEvent (event); }return handled;
    }

Copy the code

How does the parent View assign events to the TouchDelegate

In the above example, we set a TouchDelegate for the parent View at the end :((View) child-.getparent ()).settouchdelegate (TouchDelegate); So let’s look directly at this method in view.java:

    public void setTouchDelegate(TouchDelegate delegate) {
        mTouchDelegate = delegate;
    }
Copy the code

If you look at where this example is used, you can see that in the onTouchEvent method of the View, there is this logic:

        if(mTouchDelegate ! = null) {if (mTouchDelegate.onTouchEvent(event)) {
                return true; }}Copy the code

So when the parent View executes the click event, it’s going to see if there’s an mTouchDelegate, and if there is, it’s going to hand the event directly to the mTouchDelegate, and if the event is consumed in the mTouchDelegate, The parent View’s onTouchEvent is finished, that is, the parent View no longer handles the event, otherwise the parent View will still try to consume the event.


A student @7 in the comment section said that by extending the click range this way, the control will still respond to the event even if it is set to View.GONE.

Why is that? Let’s look directly at the onTouchEvent handler code in the View, and make a note of the key points, as follows:

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final floaty = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // If the View is Disabled, the event will not be distributedif ((viewFlags & ENABLED_MASK) == DISABLED) {
            if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! = 0) {setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them. return clickable; } // If (mTouchDelegate!) {if (mTouchDelegate!); = null) { if (mTouchDelegate.onTouchEvent(event)) { return true; }}... }Copy the code

So you can see that the View, when it’s handling an onTouchEvent event, is going to first determine whether the View is Enabled, and then when the View is Disabled it’s not going to proceed, and then it’s going to determine the mTouchDelegate, When the mTouchDelegate is not null, the event is directly handed over to the delegate, regardless of the visibility of the Child.

This is where we know what to do in this case. We just need to set Child to Disabled while setting it to GONE.

child.setEnabled(true);
child.setVisibility(View.GONE);
Copy the code