#自定义View 分发事件处理
##叨叨
每定义一个View除了绘制我们还需要处理他的点击事件,特别是继承View而非其他控件的时候。
同时,我们应该知道在屏幕上我们是一层套一层,那么当我们屏幕点击下去的时候,这个时候应该是哪一个去处理?这里就有了我们的分发事件。
先看一张图。
如何产生的分发事件呢?就是在我们点下屏幕的那一刻,android给我们封装好了MotionEvent
事件,有四个基本值用到:
public static final int ACTION_CANCEL = 3;
public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;
cancel
产生是非人为。。。
down
是事件开始的时候,我们点下屏幕的那一刻就是 down
move
是我们在屏幕上滑动时产生的事件
up
与 down对应,是抬起手指时事件
一般来说,我们点击到哪一个View就哪一个view去处理(消费)这些事件,所以在相应的View(特别是自定义view,ps非ViewGroup派生类)覆写 onTouchEvent
里的方法。但是既然有事件分发,那肯定存在某一个view or viewgroup中途拦截消费了这个事件。
比如,最常见的下拉刷新控件,我们在监听是否滑动到最顶层,就使用RecyclerView(ListView 或其他)父布局的刷新逻辑,这里产生的就是,当我们手指点下时,判断recyclerview是否在最顶层,并选择刷新控件是否处理(消费)该事件。如果消费了那recycler就接受不到事件也消费不了,反之就是recycler处理(消费)。
上面一段话就是,事件分发逻辑是这样的
activity --> viewgroup --> view
别问我activity哪来的,最外面那层不是activity么?你要说Fragment?它有这些事件覆写功能么?
割掉,分发过程一般有三个方法
√
表示有该方法,X
表示无啦
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | onInterceptTouchEvent | X | √ | X |
事件消费 | onTouchEvent | √ | √ | √ |
- Activity对点击事件的分发机制
- ViewGroup对点击事件的分发机制
- View对点击事件的分发机制
所以,我们需要了解每一个部分如何处理的,才能够在我们自己View里面去灵活使用。
搬一下图片,很清晰的表示了事件是怎么分发以及处理(消费)
搬运地址HERE
- 对于 dispatchTouchEvent,onTouchEvent,return true是终结事件传递。return false 是回溯到父View的onTouchEvent方法。
- ViewGroup 想把自己分发给自己的onTouchEvent,需要拦截器onInterceptTouchEvent方法return true 把事件拦截下来。
- ViewGroup 的拦截器onInterceptTouchEvent 默认是不拦截的,所以return super.onInterceptTouchEvent()=return false;
- View 没有拦截器,为了让View可以把事件分发给自己的onTouchEvent,View的dispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent。
ViewGroup和View 的dispatchTouchEvent 是做事件分发,那么这个事件可能分发出去的四个目标
注:------> 后面代表事件目标需要怎么做。
1、 自己消费,终结传递。------->return true ;
2、 给自己的onTouchEvent处理-------> 调用super.dispatchTouchEvent()系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的onTouchEvent处理。
3、 传给子View------>调用super.dispatchTouchEvent()默认实现会去调用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就会把事件传给子类。
4、 不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent------->return false;
注: 由于View没有子View所以不需要onInterceptTouchEvent 来控件是否把事件传递给子View还是拦截,所以View的事件分发调用super.dispatchTouchEvent()的时候默认把事件传给自己的onTouchEvent处理(相当于拦截),对比ViewGroup的dispatchTouchEvent 事件分发,View的事件分发没有上面提到的4个目标的第3点。
搬运完了.
我们上文知道,我们一般处理事件有down,move,up三种。
上面的图文一般针对的ACTION_DOWN
处理,那么,move和up是这样么?
了解一下,down
事件如果在没有消费,那么就会按照上文图中的步伐调用回溯直到被消费的那一层,而move
和up
两个将会在直接在dispatch
这调用onTouchEvent
也就是说,如果某一层消费了事件,那么三者的步伐不一定是相同的,特别是在消费之前的分发操作里没有return false
的存在。
实践一下,PS:这里的一切操作均为emmmm,单指操控的。
我们需要一个完整的流程,就是需要三个家伙:Activity
、ViewGroup
、View
Activty:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("MainActivity", "dispatchTouchEvent:" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("MainActivity", "onTouchEvent:" + event.getAction());
return super.onTouchEvent(event);
}
Group:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("Group", "dispatchTouchEvent:" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("Group", "onInterceptTouchEvent:" + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("Group", "onTouchEvent:" + event.getAction());
return super.onTouchEvent(event);
}
CView:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d("CView", "dispatchTouchEvent:" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("CView", "onTouchEvent:" + event.getAction());
return super.onTouchEvent(event);
}
XML布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<cn.krisez.myapplication.Group
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#000">
<cn.krisez.myapplication.CView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#ffff00"/>
</cn.krisez.myapplication.Group>
</RelativeLayout>
看一下具体内容
ok,我们点击一下,注意,点黄色区域。那里才有我们的View。
MainActivity: dispatchTouchEvent:0
Group: dispatchTouchEvent:0 onInterceptTouchEvent:0
CView: dispatchTouchEvent:0 onTouchEvent:0
Group: onTouchEvent:0
MainActivity: onTouchEvent:0
MainActivity: dispatchTouchEvent:1
MainActivity: onTouchEvent:1
我们看到我们最后还有一个dispatchTouchEvent:1
这是哪来的?看一下MotionEvent的源码不就知道了?
public static final int ACTION_UP = 1;
ok,相信你也猜到了,我们没做任何筛选处理,所以有一个UP的EVENT出现。那么,我们也可以提起一个疑问。为什么只有Activity有ACTION_UP的事件,另外几个没有,不是都没任何处理么?
这里就要想到我们的分发机制了,当我们事件产生的时候,顺序是这样的:
Activity -> ViewGroup -> View
如果这之间我们没有对事件消费,那么在走到相应的View的onTouchEvent时就会将事件回传。直到某一个消费。那么这里我们没有处理,所以我们的事件最终回到了Activity这里,相当于Activity进行消费了,那么,ACTION_MOVE 和 ACTION_UP也就不会走DOWN的路径,而是直接dispatch -> touchEvent
如果我们在ViewGroup消费了,up怎么走?
改变一下代码。
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("Group", "onTouchEvent:" + event.getAction());
return true;
}
我们直接返回TRUE。看一下LOG
MainActivity: dispatchTouchEvent:0
Group: dispatchTouchEvent:0
Group: onInterceptTouchEvent:0
CView: dispatchTouchEvent:0
CView: onTouchEvent:0
Group: onTouchEvent:0
MainActivity: dispatchTouchEvent:1
Group: dispatchTouchEvent:1
Group: onTouchEvent:1
在这里我们看到,DOWN按照顺序走到了Group这里就停了,那么我们的UP就会走到Group的分发然后直接在Group的onTouchEvent里进行消费了。
从这里我们可以得到一个结论,当DOWN在某一个地方进行消费,那么,MOVE 和 UP 会直接从dispatch分发到相应那一层的onTouchEvent
在这里,我们实验一个从左向右划的操作,进行该界面的FINISH();
我们要这样想,我们需要的是Activity的finish(),如果其他界面也有相应的操作,岂不是回传的时候就拦截了,然后我的Move就没了么?所以我们要在activity的dispatchTouchEvent方法里进行操作判断,如果是从左到右的直接finish()调,同时返回true,不进行下级分发了。
或者说,我们自定义一个ViewGroup进行拦截判断,然后在onTouchEvent里操作,就命名为MFView吧。
private int xx = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
int x = (int) ev.getX();
switch (action){
case MotionEvent.ACTION_DOWN:
xx = x;
break;
case MotionEvent.ACTION_MOVE:
if(xx - x > 50){
Log.d("Group", "onInterceptTouchEvent:" + "finish()");
return true;
}
break;
}
return false;
}
ok,实验一下,首先我要表明我要处理事件。所以在onTouchEvent里返回true。这里我们上面处理了,就不管了。不然直接传回给了activity就拿不到move和up事件了。
onInterceptTouchEvent:finish()
在这里有一个坑,就是如果其子view没有任何处理,就是默认的话,那么在这里ViewGroup的*onInterceptTouchEvent:finish()*方法不会执行move和up事件。
为什么?
了解一下,如果某一层消费了该事件且其所有子View没有一个有对事件消费的存在,那么在该View里,将在dispatch直接传递给onTouchEvent。所以,我们还要讲CView改一下。
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
再次了解下原因,当子View存在消费,那我们就需要将事件从activity一层层传递下来,在这个过程中我们会遇到viewgroup这一层,传递的时候,我们就需要再次拦截看是否需要group自己处理。所以,在group中进行从左往右滑的时候,除非子view就消费事件,不然拦截器不起任何作用。所以建议这一个手势还是放在activity里面。同时为了防止某一层消费了onTouchEvent,所以我们手势放在activity的
dispatchTouchEvent方法里。
ok。学习完了。