TouchDelegate 作用及原理

4,217 阅读4分钟

背景

Android 提供了 TouchDelegate 类用于让我们更方便的去控制子View的触控范围,包括扩大子View的点击区域 和 缩小子View的点击区域,使用很简单,下面来看一个例子吧。

使用

  • 1.获取父视图,并在界面线程上发布 Runnable。这样可以确保父视图先对子视图进行布局;
  • 2.拿到子View的实例并调用其getHitRect()方法以获取其可轻触区域的边界;
  • 3.扩展子View的可点击边界;
  • 4.实例化 TouchDelegate,将 扩展后的矩形 和 待扩展的子View 作为参数传入;
  • 5.将4中实例化的 TouchDelegate设置给父View即可。

下面的代码实现了将一个大小为 10dp x 10dp 的 ImageButton 的可点击范围在原来的基础上上下左右各扩大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);

        // 1.获取父视图,并在界面线程上发布 Runnable
        View parentView = findViewById(R.id.parent);
        parentView.post(new Runnable() {
            @Override
            public void run() {
                // 2.拿到子View的实例并调用其getHitRect()方法以获取其可轻触区域的边界
                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.扩展子View的可点击边界
                delegateRect.left -= 100;
                delegateRect.top -= 100;
                delegateRect.right += 100;
                delegateRect.bottom += 100;

                // 4.实例化 TouchDelegate,将 扩展后的矩形 和 待扩展的子View 作为参数传入
                TouchDelegate touchDelegate = new TouchDelegate(delegateRect, child);

                // 5.将4中实例化的 TouchDelegate设置给父View
                if (View.class.isInstance(child.getParent())){
                    ((View)child.getParent()).setTouchDelegate(touchDelegate);
                }
            }
        });
    }
}

使用十分简单,也没什么好说的,下面来看看其实现原理是什么。

源码分析

TouchDelegate 的内部实现

先来看看TouchDelegate,这个类很简单,主要实现在其onTouchEvent中,通过判断点击事件是否落在子View指定的扩展范围内,是的话将事件传递给mDelegateView处理,即子View,就这样。

    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()) {
            case MotionEvent.ACTION_DOWN:
                // 直接判断点击事件是否落在子View指定的扩展范围内
                mDelegateTargeted = mBounds.contains(x, y);
                sendToDelegate = mDelegateTargeted;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_MOVE:
                sendToDelegate = mDelegateTargeted;
                ......
                break;
            case MotionEvent.ACTION_CANCEL:
                sendToDelegate = mDelegateTargeted;
                mDelegateTargeted = false;
                break;
        }
        if (sendToDelegate) {
            ......
            // 若点击范围落在子View的代理区域内,则将事件传递给mDelegateView处理,即子View
            handled = mDelegateView.dispatchTouchEvent(event);
        }
        return handled;
    }

父View 如何将事件给到 TouchDelegate

上面实例中,我们在最后给父View设置了TouchDelegate((View)child.getParent()).setTouchDelegate(touchDelegate);,所以我们直接看看View.java中的这个方法:

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

看看哪里用了这个实例,可以看到在View 的 onTouchEvent方法中,有这么一段逻辑:

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

也就是父View在执行点击事件的时候,会去看当前是否有mTouchDelegate,有的话则直接将事件交给mTouchDelegate去处理,如果在mTouchDelegate中事件被消费了,那父View的onTouchEvent也就执行完了,即父View不再处理该事件,否则父View依旧会尝试去消费该事件。


评论区有个同学@七岁 说通过这种方式去扩展点击范围后,控件即使设置为View.GONE依然会响应事件。

为什么会这样呢?我们直接看下View中onTouchEvent的处理代码,关键地方做了备注,如下:

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = 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;
        // 如果View是Disabled的,事件不会继续分发
        if ((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;
        }
        // View Enabled 的情况下,不管View的可见性,直接将事件给代理类处理了
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        ......
    }

可以看到View在处理onTouchEvent事件的时候会先去判断View是否是Enabled的,当View是Disabled时则事件不再往下处理了,随后就对mTouchDelegate进行了判断,当mTouchDelegate不为null时直接将事件交给该代理去处理了,并不管此时Child的可见性。

看到这里我们也就知道了,这种情况下该怎么处理了,我们只需要在将child设置为GONE的同时将child设置为Disabled即可解决该问题。

child.setEnabled(true);
child.setVisibility(View.GONE);