在Android进阶系列1—View的事件分发体系,在这一篇的基础上,博主做的一次补充。一个知识点,从大概知道,到自己能灵活运用,中间要折腾挺久。。。
事情是这样的,博主前几天在看下拉刷新和加载更多的简易框架,其中涉及到事件冲突部分,我还是有些晕乎,一怒之下,又把事件冲突拿出来端详一番,anyway,相较之前进步是有的。
同样,本文还是总结性的,不谈细节。本文的诞生要感谢,Android onTouchEvent, onClick及onLongClick的调用机制,Android View 事件分发机制详解,onTouch事件试验(覆写onTouchEvent方法,同时设置onTouchListener), Android事件分发、View事件Listener全解析等。
这篇文章主要关注:
1. onTouchListener中onTouch方法在不同的MotionEvent状态下返回值,对onTouchEvent的影响。
2. onLongClick和onClick的关联
3. super.onTouchEvent()的作用
1.onTouch&onTouchEvent
我们都知道在onTouch中返回true的话,onTouchEvent将不会得到执行。那如果onTouch的返回根据MotionEvent的不同状态有不同返回,又是一种什么情况呢?
楼主实测发现,onTouch和onTouchEvent是平级操作,但是有先后顺序onTouch先于onTouchEvent执行。就是说不存在onTouch某个阶段返回true之后,后续事件onTouchEvent就不再执行的情况。onTouch返回false,继续执行onTouchEvent;onTouch返回true,不执行对应阶段onTouchEvent。当然,要是存在onTouch返回false,onTouchEvent也返回false这种均不消费事件的情况,除非改写了默认的dispatchEvent,不然事件将不再交给onTouch执行了。
public class TestActivity extends Activity implements View.OnTouchListener, View.OnClickListener, View.OnLongClickListener {
private TestButton mButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mButton = (TestButton) this.findViewById(R.id.my_btn);
mButton.setOnTouchListener(this);
mButton.setOnLongClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.v("-onTouch", "------ACTION_DOWN-----");
// break;
// return true;
return false;
}
case MotionEvent.ACTION_MOVE: {
Log.v("-onTouch", "------ACTION_MOVE-----");
// break;
// return true;
return true;
}
case MotionEvent.ACTION_UP: {
Log.v("-onTouch", "------ACTION_UP-----");
// break;
return false;
// return false;
}
}
return false;
}
@Override
public void onClick(View v) {
Log.i(null, "Button--onClick--" + v);
}
@Override
public boolean onLongClick(View v) {
Log.i(null, "Button--onLongClick--" + v);
return true;
}
}
public class TestButton extends Button {
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, "onTouchEvent-- action=" + event.getAction());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.v("-MyButton-", "------ACTION_DOWN-----");
// return true;
return super.onTouchEvent(event);
// return false;
}
case MotionEvent.ACTION_MOVE: {
Log.v("-MyButton-", "------ACTION_MOVE-----");
return true;
// return super.onTouchEvent(event);
// return false;
}
case MotionEvent.ACTION_UP: {
Log.v("-MyButton-", "------ACTION_UP-----");
// return true;
// return false;
return super.onTouchEvent(event);
}
}
return false;
}
}
这里有个要注意的自定义控件是因为重写onTouchEvent造成的误触发问题。如果onTouchEvent处理了DOWN事件,但是后续事件到达不了,则按钮等可点击控件会一直处于按下状态。给用户造成困扰。这种情况下,记得排查下是不是DOWN的处理方式不正确。
2.onLongClick&onClick
onLongClick和onClick的触发都是在onTouchEvent中依赖DOWN、MOVE、UP三个阶段实现的。
DOWN阶段:
首先设置标志为PREPRESSED,设置mHasPerformedLongPress=false,然后发出一个115ms后的mPendingCheckForTap,如果115ms内抬起手指,触发了UP,则不会触发click事件,并且最终执行的是UnsetPressedState对象,setPressed(false)将setPress的传递下去;这种情况很少发生,毕竟手指的反应速度达到0.115S,简直开挂!!!
如果115ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-115ms的,检测长按任务消息,也就是说长按的判断时间标准是从按下屏幕后500ms。
如果500ms内没有发生up事件,则会触发LongClickListener。LongClickListener不为null(用户监听了LongClickListener),执行回调,若LongClickListener.onClick返回true,mHasPerformedLongPress设置为true,不能触发后续的onClick;否则mHasPerformedLongPress依然为false,可以继续触发onClick。
MOVE阶段
主要就是检测用户是否划出控件,如果划出了:
1. 115ms内,直接移除mPendingCheckForTap;
2. 115ms后,则将标志中的PRESSED去除,同时移除长按的检查:removeLongPressCallback();
UP阶段
- 如果115ms内,触发UP,此时标志为PREPRESSED,则执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;=
- 如果是115ms-500ms间,即长按还未发生,则首先移除长按检测,执行onClick回调;
- 如果是500ms以后,那么有两种情况:
- 设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;
- 没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;
- 最后执行mUnsetPressedState.run(),将setPressed传递下去,然后将PRESSED标识去除;
为什么onLongClick和onClick可以被同时触发,得理解Android对事件处理的所谓消费(consume)概念,一个用户的操作会被传递到不同的View控件或者同一个控件的不同监听方法处理,任何一个接收并处理了该次事件的方法如果在处理完后返回了true,那么该次event就算被完全处理了,其他的View或者监听方法就不会再有机会处理该event了。当然如果自行复写相关方法处理事件完毕之后,你仍想手动返回false。。。很好,这很容易迷惑
onLongClick的发生是由单独的线程完成的,并且在ACTION_UP之前,而onClick的发生是在ACTION_UP后,因此同一次用户touch操作就有可能同时经历onLongClick和onClick阶段。当然就像上面所述的“consume“原则一样,如果在onLongClick()方法的最后return true,那么onClick事件就没有机会被触发了。
3.super.onTouchEvent
有时候我们自定义View,复写onTouchEvent,返回值百般思考后确定了true或者false,庆幸事件冲突终于解决。使用的时候发现设置的OnClickListener不起作用,这可如何是好???想想问题出在哪儿,View.onTouchEvent里面会调用方法回调设置的onClick方法,你自定义的View又没手动执行performLongClick()或performClick(),也不super.onTouchEvent(),当然就不响应click操作。
所以,弄清View、ViewGroup的onDispatchEvent、onTouchEvent,onInterceptEvent具体进行了哪些操作很重要。看到过infoQ上翻译的一篇文章“dispatchTouchEvent方法用于事件的分发,Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发,事件没有被消费。返回false则继续往下分发“,对dispatchTouchEvent就有误解,因果倒置,子View消费事件与否是因,它的返回值是果,具体的可见Android进阶系列1—View的事件分发体系,有比较详细的描述。
最后
好了,第二篇关于View的事件分发就总结完了,这是在第一次的基础上的查漏补缺。
很惭愧,做了一点微小的贡献!