在写自定义view的时候经常会遇到需要为其添加长按事件的情况,当然,这里分几种情况,比如该自定义view如果是继承自listView或者是gridView的话,可以直接为其建立一个长按监听器:
listview.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemClick(AdapterView<?> parent, View view,int position, long id) {
// Do something
return true.
}
但是,如果设立长按监听之后会与短按监听冲突,这里有两种情形,第一种是如果长按监听是弹出一个dialog之类的窗口视图的话,那么此时不会再进入端按监听的形式,此时可以不用对监听冲突进行处理。当然,第二种情形就是窗口视图之外的操作,这时候可以设置一个布尔型的全局变量,因为长按时一般会先触发长按事件,然后再触发短按事件,我们在二者中分别给布尔变量赋予不用的值并在短按事件里做个判断就行了,伪代码如下:
gridView.setOnItemLongClickListener(new OnItemLongClickListener(){//设置事件监听(长按)
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
longPress=true;
//执行操作
}
}
//设置事件监听(短按)
gridView.setOnItemClickListener(new OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
If(longPress)
{
longPress=false;
break;
}
else
{
longPress=false;
//执行操作
}
}
}
可是,在实际操作中,比如公司希望你开发一个公用的自定义view,并且因为业务需求刚好需要用到长按事件的话,此时如果还是采用需要人为的设立监听器的方式,会违背了公用view的基本原则,因此比较妥当的方式是在自定义view的分发过程中对其长按操作进行判断,而长按事件的发生可以采用runnable和handler(view里有自带的handler)的形式,这是目前通用的处理方式,也是本文的重点。本文还涉及到view的分发机制,如果有不了解的同学可以先自行百度哈,过一段时间我也会写博客来对其分发事件进行分析
先来总结一下几种常见的长按实现,第一种形式:
public class LongPressView1 extends View{
private int mLastMotionX, mLastMotionY;
//是否移动了
private boolean isMoved;
//是否释放了
private boolean isReleased;
//长按的runnable
private Runnable mLongPressRunnable;
//移动的阈值
private static final int TOUCH_SLOP = 20;
public LongPressView1(Context context) {
super(context);
mLongPressRunnable = new Runnable() {
@Override
public void run() {
performLongClick();
}
};
}
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
mLastMotionY = y;
isReleased = false;
isMoved = false;
postDelayed(mLongPressRunnable,ViewConfiguration.getLongPressTimeout());
break;
case MotionEvent.ACTION_MOVE:
if(isMoved) break;
if(Math.abs(mLastMotionX-x) > TOUCH_SLOP
|| Math.abs(mLastMotionY-y) > TOUCH_SLOP) {
//移动超过阈值,则表示移动了
isMoved = true;
removeCallbacks(mLongPressRunnable);
}
break;
case MotionEvent.ACTION_UP:
//释放了
isReleased = true;
removeCallbacks(mLongPressRunnable);
break;
}
return true;
}
}
其实原理很简单,首先先实现一个runnable接口作为长按发生之后的处理时间。然后就来到了view的事件分发机制的起点dispatchTouchEvent方法,该方法实现了分发view的三种类型的点击事件:手指触摸,移动,和手指抬起。很容易联想到长按事件是手指按住某个view长时间不动,因此主要的处理逻辑在MotionEvent.ACTION_DOWN,其中使用了postDelayed来实现连续触摸给定的值之后触发之前写好的 mLongPressRunnable 事件,postDelayed的操作实现跟view内部的handler和消息队列有关,具体的原理可以参见我的前几篇博客。然后在MotionEvent.ACTION_MOVE,MotionEvent.ACTION_UP中调用 removeCallbacks来取消长按任务的排队。具体的运作原理是如果点击了view,只是到了DOWN的流程,这时候postDelayed开始执行, mLongPressRunnable开始在消息队列中排队,预定执行时间是ViewConfiguration.getLongPressTimeout()之后,如果,此时在小于ViewConfiguration.getLongPressTimeout()的时间里用户移动view或者手指抬起,那么就会到ACTION_MOVE或者ACTION_UP下处理,这是直接调用 removeCallbacks将还没到执行时间的任务mLongPressRunnable给取消了,具体思路就是这样。
当然长按事件也可以采用第二种方式实现:
public class LongPressView1 extends View{
private int mLastMotionX, mLastMotionY;
//是否移动了
private boolean isMoved;
//是否释放了
private boolean isReleased;
//长按的runnable
private Runnable mLongPressRunnable;
//移动的阈值
private static final int TOUCH_SLOP = 20;
public LongPressView1(Context context) {
super(context);
mLongPressRunnable = new Runnable() {
@Override
public void run() {
if(isReleased || isMoved) return;
performLongClick();
}
};
}
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
mLastMotionY = y;
isReleased = false;
isMoved = false;
postDelayed(mLongPressRunnable,ViewConfiguration.getLongPressTimeout());
break;
case MotionEvent.ACTION_MOVE:
if(isMoved) break;
if(Math.abs(mLastMotionX-x) > TOUCH_SLOP
|| Math.abs(mLastMotionY-y) > TOUCH_SLOP) {
//移动超过阈值,则表示移动了
isMoved = true;
removeCallbacks(mLongPressRunnable);
}
break;
case MotionEvent.ACTION_UP:
//释放了
isReleased = true;
removeCallbacks(mLongPressRunnable);
break;
}
return true;
}
}
这里很简单,就不分析了。
此外,还有最后一点疑问,就是判断长按事件是一定要放在dispatchTouchEvent方法里的,对于view的分发过程中的另外两个方法,比如onTouchEvent和onInterceptTouchEvent方法。为啥放在这两个方法里会不合适呢?其实在《Android开发艺术探索》里的一段伪代码已经解释的很清楚了:
上述代码是viewGroup中独有的,在view里并没有onInterceptTouchEven,因为拦不拦截是由viewGroup来决定的。viewGroup中的onInterceptTouchEven的默认值是false,只有这样才能实现每次都将事件下发给View去处理,同时ViewGroup的onTouchEvent默认值是false,而在view里,onTouchEvent返回默认值是true.这样才能执行多次touch事件,否则的话在view里一旦返回来false,那么以后的事件都不会再下发给该view了。
如果我们在onInterceptTouchEven里来判断当前的事件是不是长按事件,那么,如果在DOWN中我们判断了该事件为长按事件,那么讲道理,会调用上面例子中的 postDelayed()方法来实现某固定时间之后的操作,此时任务就在消息队列里排队了。
如果我们在onInterceptTouchEven里返回了false,那么此时后续的MOVE,UP事件也会先传递给该viewGroup,但是已经不会再交由onInterceptTouchEven了,而是将三种触摸事件凑齐之后一起发给view的onTouchEvent去处理(参见伪代码)。所以此时在MOVE,UP里写的removeCallbacks都本就没有被调用。。因此长按事件判断失败。
另一种情况,我们在onInterceptTouchEven里返回了true,那么我们首先会在onInterceptTouchEven里接收DOWN事件,此时依然会调用上面例子中的 postDelayed()方法来实现某固定时间之后的操作,可是由于返回的是true,由伪代码可知后续的MOVE,UP等事件都不会再传递给onInterceptTouchEven,而是和DOWN事件一样直接传递给viewGroup的onTouchEvent()处理,所以此时下面的子view将接收不到任何事件。故而简单的可以认为onInterceptTouchEven只会拦截DOWN事件,其他的两个时间实际上和它没有关系了。
这里再补充一点,如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件会被交还给分发给它的viewGroup,由viewGroup决定是继续往下分发给其他子view还是交给activity处理。如果最终需要处理事件的view 的onTouchEvent()返回了true,那么后续事件都将继续传递给该view的onTouchEvent()处理。
因此得出最重要的总结,如果我们想要在自定义view的特殊事件进行处理的话,比如例子中的长按判断,最好是将其写在dispatchTouchEvent中,这样才能保证每次事件到来时都能对其进行分类处理,因为onInterceptTouchEven和onTouchEvent并不是每次都会调用的。