(1)事件分发机制学习
只要触摸了屏幕,就会触发dispatchTouchEvent,并且从布局的底层往上执行。也就是最底层的ViewGroup往最顶层的view的手势事件分发。
View的dispatchTouchEvent方法
publicboolean dispatchTouchEvent(MotionEvent event){
if(mOnTouchListener=null&&(mViewFlags&ENABLE_MASK)==ENABLED
&&mOnTouchListener.onTouch(this,event))
{
returntrue;
}
returnonTouchEvent(event);
}
mOnTouchListener:只要控件设置了setOnTouchListener,它就不为空。第一个条件返回true.
(mViewFlags&ENABLE_MASK)==ENABLED:控件是否可用,一般控件默认为可用,返回true.
控件可以通过javacode中setEnabled(false),或者xml文件中android:enabled=”false”来设置不可用。
一般该条件不用管。默认为true就ok。
MonTouchListener.onTouch(this,event):这里指的是setOnTouchListener(newonTouchListener(){
publicboolean onTouch(View v,MotionEvent event)
{
returnfalse/true;//默认是给false.
}
});
如果这三个条件全部满足,dispatchTouchEvent直接返回true.不会执行returnonTouchEvent(event)这句代码。
自然也就不会执行View的onTouchEvent(MotionEventevent)方法。
而mOnClickListener.onClick(this)方法在View的onTouchEvent(event)方法中进行判断是否执行的。所以如果没有执行到returnonTouchEvent(event)这句,onClick()是绝对不会执行的。
View的onTouchEvent(MotionEventevent)比较长,节选处比较重要的几条。
publicboolean onTouchEvent(MotionEvent event){
…
if(viewFlags&CLICKABLE)==CLICKABLE||(viewFlags&LONG_CLICKABLE)==LONG_CLICKABLE))
{
switch(event.getAction()){
caseMotionEvent.ACTION_UP:
…
performClick();
//performClick()中就执行了mOnClickListener.onClick().前提你得先给组件设置setOnClickListener(...)。
…
break;
caseMotionEvent.ACTION_DOWN:
…
break;
caseMotionEvent.ACTION_CANCEL:
…
break;
caseMotionEvent.ACTION_MOVE:
…
break
}
returntrue;
}
returnfalse;
}
可以获取的信息是如果View可以点击或者可以长按。就会执行if中的语句,并且返回true(这句非常重要,要是if条件没满足的话,就会返回false)。然后View设置了setOnClickListener,就可以响应View的onClick()事件了。
所以对于ImageView,如果onTouch()返回false,因为它默认不可点击。所以它只能响应MotionEvent.ACTION_DOWN.如果想让它ACTION_UP,ACTION_MOVE都能响应。有两种方式,要么onTouch()返回true.或者设置它为可点击的。
ViewGroup的相关方法
onInterceptTouchEvent(MotionEventevent)
dispatchTouchEvent(MotionEventevent)(重写View的该方法)
ViewGroup的onTouchEvent(MotionEventevent)就是View的onTouchEvent(MotionEventevent).
publicboolean onInterceptTouchEvent(MotionEvent event){
returnfalse;
}
该方法默认是返回false的。默认ViewGroup容器是不会拦截手势事件的。
除非你重写returntrue.
Publicboolean dispatchTouchEvent(MotionEvent ev){
…
if(disallowInterceptTouchEvent||!onInterceptTouchEvent(ev))
//disallowInterceptTouchEvent:是否禁用拦截手势事件这项功能,一般不用管它,默认是false.也就是允许这项功
//能
//!onInterceptTouchEvent(ev):它为true的意思是不拦截手势事件。默认也是不拦截。
{
...
if(frame.contais(scrolledIntX,scrolledIntY)){
//这句判断当前遍历的View是否是点击的View
//注意,一旦找到了,并且child.dispatchTouchEvent(ev)返回true,这个遍历也就结束了。
//假如两个button,重叠在一起,在同一个ViewGroup.这意味着即使重叠在一起,也只有一个view执行了dispatchTouchEvent方法。而且是后放的那个。看下面的for循环可知。
//假如一个ImageView放在Button上面,先执行到ImageView的dispatchTouchEvent,可以响应ImageView的setOnTouchListener的onTouch()方法中的MotionEvent.ACTION_DOWN.如果默认让onTouch()方法返回false,因为ImageView不可点击,ImageView的onTouchEvent也会返回false.最终ImageView的dispatchTouchEvent也返回false.然后ImageView下面的Button可以响应到点击事件和手势事件。
…
finalint count=mChildrenCount;
for(inti=count-1;i>0;i--){
if(child.dispatchTouchEvent(ev)){
mMotionTarget=child;
returntrue;
}
}
}
}
//如果拦截了手势事件,也就是上面的if条件不满足。或者点击的位置没在子View的范围内,也就是frame.contais(scrolledIntX,scrolledIntY)返回false.这样才有机会执行ViewGroup的dispatchTouchEvent()方法(其实也是View的dispatchTouchEvent)。
...
if(target==null){
…
returnsuper.dispatchTouchEvent(ev);
//这里可以理解为把View的dispatchTouchEvent(ev)的那部分代码搬到这里,那么那里面的onTouch(this,ev)中的this正好指的是当前ViewGroup的实例。
}
}
(2)事件分发测试,加深理解
-
对于堆叠在一起的控件,必然同时只能其中一个控件响应onClick。
if(!isClickable()) {
-
Android:setClickable 一定要放在按钮的setOnClickListener事件之后,因为setOnClickListener事件会去重写Viewv,也就是会把setClickable设为true.
View中的源码如下。
publicvoidsetOnClickListener(OnClickListener l) {
setClickable(true);
}
getListenerInfo().mOnClickListener= l;
}
-
系统Button默认android:clickable=”true”;android:longClickabel=”false”;
-
当Button的clickable,longClickable属性都为false时,Button会一直显示没被按下的样子。
-
View首先调用一次dispatchTouchEvent,而第一次响应的必然是ACTION_DOWN.
-
View的dispatchTouchEvent返回为true的时候,才回响应后续的ACTION_MOVE,ACTION_UP等事件。比如想让TextView响应ACTION_DOWN,ACTION_MOVE,ACTION_UP.有两种方式,
方法一是mTextView设置Touch监听的时候,把onTouch的返回值设置为true.(默认是false)。
方法一设置后就不响应onClick事件。
方法二是把mTextView的clickable属性设置为true.可同时响应onTouch和onClick.
-
假设Button设置了手势监听和onClick.在满足ACTION_DOWN的位置在Button范围内,ACTION_UP的位置在Button范围外。手势监听的onTouch中的ACTION_UP会执行,但是onClick并不会执行。只有ACTION_UP的位置也在Button范围内,并且ACTION_MOVE从来没有出去过Button范围外。
总结来说,onTouch,DOWN,UP,MOVE都执行的条件是DOWN的位置在控件范围内。
onClick执行的条件是DOWN,UP,MOVE的位置一直都在控件范围内。一旦MOVE除控件范围外,即使UP的位置在控件范围内,onClick也不会执行。
(3)GridView,ListView事件分发探索。
GridView,ListView均继承于AbsListView。AbsListView重写了onTouchEvent方法。
首先明确GridView是容器。我们自定义的item假设是一个RelativeLayout中放一个Button.
一般容器是默认不拦截事件的。所以事件会从GridView分发到RelativeLayout然后到Button.
因为Button默认clickable为true.导致Button的onTouchEvent返回true,进而导致Button的dispatchTouchEvent返回true.不会执行容器的dispatchTouchEvent.
而我们想要调用GridView的setOnItemClickListener这个方法。我们需要让事件分发到GridView.
我们可以把GridView的每个部分理解成A容器--B容器--C组件。也就是要让事件分发到A容器。
所以我们把Button的android:clickable属性设置为false.默认Button的longClickable属性也是false.默认onTouch方法也是返回false.
此时事件从C组件分发到B容器,B容器是我们用的 RelativeLayoutorLinearLayout.不设置监听事件的话,会继续分发到A容器。
我们发现此时GridView的setOnItemClickListener依然没有生效。原因是因为GridView和ListView继承的AbsListView重写了onTouchEvent.
查看源码,可以发现AbsListView的onTouchEvent中
publicbooleanonTouchEvent(MotionEvent ev) {
….
caseMotionEvent.ACTION_UP:{
onTouchUp(ev);
break;
….
}
privatevoidonTouchUp(MotionEvent ev) {
….
if(inList && !child.hasFocusable()) {
if(mPerformClick== null){
mPerformClick= newPerformClick();
}
….
if(!mDataChanged && mAdapter.isEnabled(motionPosition)){
mTouchMode= TOUCH_MODE_TAP;
setSelectedPositionInt(mMotionPosition);
layoutChildren();
child.setPressed(true);
positionSelector(mMotionPosition,child);
setPressed(true);
…..
mTouchModeReset= newRunnable() {
@Override
publicvoidrun() {
mTouchModeReset= null;
mTouchMode= TOUCH_MODE_REST;
child.setPressed(false);
setPressed(false);
if(!mDataChanged && isAttachedToWindow()) {
performClick.run();
}
}
};
….
}
...}
源码中标示出!child.hasFocusable()要为true才能响应点击事件。也就是B容器,C组件不能获得焦点。
所以想要事件分发到A容器。我们还要把Button的android:focusable属性设置为false.B容器默认focusable为false.(待确认下,应该是的)
也就是Button中以下属性要确认,clickable为false,longClickabel为false,focusable为false.
我们发现可以响应GridView的setOnItemClickListener后,但是C组件竟然也有按下和释放的效果了。
明明我们在设置clickable为false,longClickabel为false后就应该Button无法点击,一直处于未点击的效果不变的。
原因是源码中有child.setPressed(true)和child.setPressed(false).
而且源码中有setPressed(true)和setPressed(false).
所以A容器,B容器,C组件(可以是多个,已经验证,但要设置好让事件分发到A容器)都会有按下和释放的效果,如果你有设置。