一、关于View事件传递
Android 事件主要有两种,KeyEvent和TouchEvent,其中TouchEvent事件相对复杂。
- KeyEvent传递,这种相对简单,只要找到当前的foucs View,因此,想接收事件的View必须Focusable。
- TouchEvent传递,涉及父布局拦截、子布局拦截两种状态,处理起来相对复杂。
二、TouchEvent事件传递
【1】事件的核心点
涉及事件传递问题,一般都自定义ViewGroup的情况或者滑动ViewGroup冲突时才会有相应的需求,事件传递的核心是ViewGroup而不是子View。TouchEvent传递事件有两个过程,一个是 分发过程 ,一个是 处理过程 ,单独的说具体原理实际上很难处理,因此,我们这里通过情景分析方式来说明各种需求事件如何传递。
【2】事件的起点
ACTION_DOWN作为事件的起点,是事件分发和处理过程中不能忽视的因素,因为一旦ACTION_DOWN被某View处理(这里的“处理”是指onTouchEvent返回值为true),因为这后续的事件 直接 由该View处理,除非该View主动放弃,主动放弃需调用父布局的dispatchTouchEvent发送ACTION_CANCEL。
【3】事件转移
本文虽然是事件传递,但事件转移也是非常重要的,我们可能遇到的情景是滑动子View,当滑动到顶部时需要将事件转移给父布局,让父布局滑动。当然,Android 官方提供了NestedScrolling机制,也是一种很不错的解决方式。但我们还需要了解另一种方式,流程如下。
结合【2】,我们转移事件之前,需要调用父布局的dispatchTouchEvent发送ACTION_CANCEL,接着通过父布局的dispatchTouchEvent发送ACTION_DOWN,这样相当于创建了一个事件的起点,通过这种方式,可以让父布局去捕获事件ACTION_DOWN事件,从而完成转移。
【4】情景分析
在下面的论述中,我们用ChildView指代子View,ParentLayout指代父View。
情景一:parentLayout不需要事件,ChildView需要事件
ParentLayout不做任何处理,ChildView在onTouchEvent中处理ACTION_DOWN返回true,这样后续事件由ChildView处理
情景二:ParentLayout需要事件,ChildView不需要事件
ChildView不做任何处理,parentLayout 中的onInteceptTouchEvent和onTouchEvent中处理ACTION_DOWN返回true,后续事件由parentLayout处理
情景三:用户视觉中,谁先被点击,事件交由谁处理
父布局dispatchTouchEvent 中需要判断点击位置,如果点击位置在用户视觉中不属于它自己,在onInteceptTouchEvent中不要拦截该事件,这样传递事件给ChildView,否则走默认逻辑。
情景四:通过滑动方向转移事件
这种是最复杂的事件传递方式,需要父布局在dispatchTouchEvent中直接处理事件, 初始状态 ,接收到ACTION_DOWN,优先拦截,满足分发给ChildView,满足自己处理事件的条件时自己来拦截。最为典型的时时SlidingMenu + ScrollView的实现,作为父布局的SlidingMenu需要和ScrollView以某种方式联动,当然不联动也是可以的。
不联动方式:
父布局检测到该滑动方向不由自己处理,然后修改当前的事件为ACTION_DOWN, 记录状态 ,不让onInteceptTouchEvent拦截下一个ACTION_DOWN ,最后调用super.dispatchTouchEvent分发事件。
父布局检测到该事件可以由自己处理,然后修改当前事件为ACTION_DWON, 记录状态 ,让onInteceptTouchEvent拦截ACTION_DOWN,最后调用super.dispatchTouchEvent分发事件。
状态重置非常重要,我们需要在parentLayout的dispatchTouchEvent处理ACTION_UP或者ACTION_CANCEL时让状态值恢复到原来的状态。
联动方式:
联动方式主要是互相通过ChildView和ParentLayout互相实现某种接口进行通信,可以说增加View间的耦合,降低View的通用性,但这种方式无疑是最简单的,同样也是最好用的,Android 官方推荐的NestedScrolling这种机制,目前NestedScrollView和RecyclerView都实现了,通过这种方式让很多复杂的View实现变得很简单,这种方式也越来越多的在View组件中出现。
情景五:事件双响应
parentLayout 和childView 同时响应事件,这种情况下parentLayout 需要在onInteceptTouchEvent中拦截ACTION_DOWN,同时主动调用dispatchTransformTouchEvent分发事件给View
三、总结
-
ViewGroup拦截事件需要两次处理TouchEvent,分别是onInteceptTouchEvent和onTouchEvent
-
子View拦截事件只需要子啊onTouchEvent进行处理
- 事件分发的主动权由ViewGroup掌握
- 事件一旦被一方捕获,那么后续事件只能由捕获方处理
- 事件转移之前需要触发ACTION_CANCEL
四、使用场景案例收集
【1】EditText分为编辑态和非编辑态,我们想做到编辑态时让EditTex能正常拦截焦点,非编辑态能处理点击事件。
网上有很多方法,比如修改bufferType,设置setOnTouchFocusable等,实际上都能实现,这里我们提供另一种方案:
当EditText 为可编辑时(enable=true),父布局不拦截事件,当EditText为不可编辑态(enable=false)时,父布局拦截事件,让父布局代理点击事件
public class EditInputLayout extends FrameLayout {
private boolean mInterceptTouchEvent = false;
public EditInputLayout(@NonNull Context context) {
super(context);
}
public EditInputLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public EditInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params, boolean preventRequestLayout) {
if (!(child instanceof EditText)) {
throw new IllegalArgumentException("Child Widget must be EditText in this layout");
}
return super.addViewInLayout(child, index, params, preventRequestLayout);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int actionMasked = ev.getAction() & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
View child = findClickTarget(ev);
//处理当前事件,保留原始状态下的
if (child != null && !child.isEnabled() || super.onInterceptTouchEvent(ev)) {
mInterceptTouchEvent = true;
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
super.onTouchEvent(event);//保证原有逻辑执行
return mInterceptTouchEvent ;
}
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_OUTSIDE) {
mInterceptTouchEvent = false;
}
return super.onTouchEvent(event);
}
private View findClickTarget(MotionEvent ev) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
float x = ev.getX();
float y = ev.getY();
if (child.getX() < x && (child.getX() + child.getWidth()) > x) {
if (child.getY() < y && (child.getY() + child.getHeight()) > y) {
return child;
}
}
}
return null;
}
}