背景
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);