今天在做项目的时候遇到一个问题,就是怎么让ListView中的item点击后其内部的内容跟着变色,比如现在我的item布局中有一个TextView,现在点击item的时候,让其背景色发生改变,这个我们可以为item布局背景定义一个selctor.xml就可以了,但是现在的问题是item内容布局中的TextView中的内容也要跟着变色,这个立马想到了触摸监听器onTouch方法,只需要在ACTION_UP和ACTION_DOWN中实现对TextView的字体颜色的改变就可以了,但是现在遇到一个问题,就是当我实现了onTouch方法的时候,ListView的点击事件就没有了(setOnItemClick方法),上网查看了很多内容就是说onTouch方法的返回值问题,如果onTouch方法返回true的话,setOnItemClick方法就不执行了,但是我把onTouch的返回值设置成false之后,出现一个问题就是onTouch方法只执行了一次就是ACTION_DOWN中的代码,而ACTION_UP中的代码并没有执行,很是纠结的时候最后找到的方法就是在ListView的适配器中的getView方法中对contentView实现onClick方法就可以了,具体原因下面来分析一下:
问题是解决了,但是内部的原理要弄清楚一下,于是查看了View和ViewGroup中的关于onTouch,onClick方法的源代码,问题总算弄明白了:
现在主要有两个问题:
(1) 第一个问题是控件本省的onTouch和onClick方法的执行冲突
(2) 第二个问题是子控件和父控件之间的onTouch执行过程的分析
首先自定义一个MyLayout继承LinearLayout,重写dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent方法,代码如下:
package com.bbdtek.demo; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; public class MyLayout extends LinearLayout { @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("Demo:","父View的dispatchTouchEvent方法执行了"); return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.e("Demo:","父View的onInterceptTouchEvent方法执行了"); return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e("Demo:","父View的onTouchEvent方法执行了"); return super.onTouchEvent(event); } public MyLayout(Context context) { super(context); } public MyLayout(Context context,AttributeSet attr) { super(context,attr); } }
在这三个方法中打印一段信息,有助于查看这三个方法的执行流程
下面是自定义一个MyTextView继承TextView,重写dispatchTouchEvent,onTouchEvent方法(这里没有onInterceptTouchEvent方法,原因很简单,就是TextView是一个View没有子View了,不需要拦截信息了)代码如下:
package com.bbdtek.demo; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.widget.TextView; public class MyTextView extends TextView{ @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("Demo:","子View的dispatchTouchEvent方法执行了"); return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e("Demo:","子View的onTouchEvent方法执行了"); return super.onTouchEvent(event); } public MyTextView(Context context) { super(context); } public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); } public MyTextView(Context context, AttributeSet attrs,int defStyle) { super(context, attrs, defStyle); } }
在方法中输出log信息,便于查看方法的执行流程
定义的一个布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <com.bbdtek.demo.MyLayout android:id="@+id/layout" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <com.bbdtek.demo.MyButton android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点我"/> <com.bbdtek.demo.MyTextView android:id="@+id/txt1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="我是个好人我是个好人呢我是啊速度将法律上的经费福建省的雷锋精神多了爽肤水两地分居SD卡飓风桑迪快乐分类数据的法律进多少" android:textSize="25dp"/> </com.bbdtek.demo.MyLayout> </LinearLayout>
MyLayout中包含一个MyTextView控件:
下面来看一下测试代码:
package com.bbdtek.demo; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; public class AndroidDemoActivity extends Activity{ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); MyTextView txt1 = (MyTextView)findViewById(R.id.txt1); //MyLayout layout = (MyLayout)findViewById(R.id.layout); txt1.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { Log.e("Demo:","点击了txt1"); }}); //给MyTextView定义一个onTouchListener监听器 txt1.setOnTouchListener(new OnTouchListener(){ @Override public boolean onTouch(View v, MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN){ Log.e("Demo:","txt1按下"); }else if(event.getAction() == MotionEvent.ACTION_UP){ Log.e("Demo:","txt1弹起"); } return false; }}); //给MyLayout定义一个onTouchListener监听器 /*layout.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { Log.e("Demo:","点击了父layout"); }});*/ } }
看一下运行结果:
可以看到先执行的是MyTextView的dispatchTouchEvent方法(这个方法是每次触发onTouch方法都会执行的),然后是执行了onTouch方法中的ACTION_UP中的代码,然后执行了MyTextView中的onTouchEvent方法,当用户弹起手指的时候又一次执行了这样的一个过程,最后就是执行了onClick方法,在这里就来看一下onTouchEvent中的源代码:
因为TextView继承View,可以查看View中的onTouchEvent方法,在这个方法中又调用了ViewGroup中的onTouchEvent方法,下面在来看一下ViewGroup中的onTouchEvent方法:
在ACTION_UP中的这段代码就是执行了onClick方法,具体可以看一下performClick方法解释:
mOnClickListener就是OnClickListener监听器,执行了onClick方法,所以上面的onClick方法是在ACTION_UP之后执行了。
下面在来看一下这种情况,现在把onTouch方法的返回值改成true:看一下执行结果:
可以看到首先还是执行了MyTextView的dispatchTouchEvent方法,然后执行了onTouch中的ACTION_DOWN代码,同样当用户的弹起手指的时候执行了同样的过程,现在的问题是这样的执行过程为什么和上面的不一样呢?
首先来看一下有哪些不一样的地方:
第一个不一样的地方就是”子View的onTouchEvent方法执行了“这句话没有执行,那就是MyTextView中的onTouchEvent方法没有执行了,来看一下源码:
在View中的dispatchTouchEvent方法中可以看到,是先执行OnTouchListener监听器中的onTouch方法,如果onTouchListener不为null,并且onTouch方法返回false的时候才执行onTouchEvent方法,现在我们把onTouch方法的返回值变成了true,所以onTouchEvent方法就不执行了。这里一定要注意onTouchEvent方法和onTouch方法的区别。
第二个不一样的地方就是onClick方法没有执行了,对于这样的问题,在源代码中没有找到答案,但是记住一点就可以了那就是如果onTouch方法返回true,说明View这次消费了这次事件,所以就不会再执行后续的onClick方法了,
从上面的例子可以看出来:onClick方法的执行时依赖于onTouch方法的,下面就总结一下onClick和onTouch两个方法之间的执行关系:
如果该View 是disable 状态:那么给它加上pressed标志位,重绘一下(在屏幕上显示为灰显按下去的效果)
如果该View不是disable状态,并且是clickable,那么在ACTION_DOWN的时候会加上pressed标志位,并启动一个timer(长按事件onLongClickListener)。重绘一下(显示出按下去的效果)。此时还是MOVE事件监视该View的状态,如果这个MOVE滑出了该View的范围,那么会复位View的click状态。UP事件时,会检查是否已经执行过LongPress,如果已经执行了LongPress,那么就不执行了Click,反之,会取消掉LongPress的Timer,然后在执行Click。
在UP事件的最后,做一下收尾处理,完事。
所以TouchEvent是Click 和 LongPress的底层实现,View的派生类如Button等等就不需要重写OnTouchEvent。
现在来看一下第二个问题就是:父控件和子控件的onTouch方法的执行流程:
首先来看一下测试代码:
package com.bbdtek.demo; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; public class AndroidDemoActivity extends Activity{ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); MyTextView txt1 = (MyTextView)findViewById(R.id.txt1); MyLayout layout = (MyLayout)findViewById(R.id.layout); txt1.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { Log.e("Demo:","点击了txt1"); }}); //给MyTextView定义一个onTouchListener监听器 txt1.setOnTouchListener(new OnTouchListener(){ @Override public boolean onTouch(View v, MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN){ Log.e("Demo:","txt1按下"); }else if(event.getAction() == MotionEvent.ACTION_UP){ Log.e("Demo:","txt1弹起"); } return false; }}); //给MyTextView定义一个onTouchListener监听器 layout.setOnTouchListener(new OnTouchListener(){ @Override public boolean onTouch(View v, MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN){ Log.e("Demo:","父layout按下"); }else if(event.getAction() == MotionEvent.ACTION_UP){ Log.e("Demo:","父layout弹起"); } return false; }}); //给MyLayout定义一个onTouchListener监听器 layout.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { Log.e("Demo:","点击了父layout"); }}); } }
点击MyTextView后观察log信息如下:
这样就可以清楚的看到onTouch的执行过程了:
这三种情况就详细说明了这个执行过程,其中dispatchTouchEvent方法没有标注,比较简单,首先会执行父控件的dispatchTouchEvent方法,下面在来看一种情况,就是在测试代码中将这些代码注释:
/*txt1.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Log.e(“Demo:”,”点击了txt1″);
}});*/
看一下运行结果:
这里就是把MyTextView的点击监听器注释了,但是运行的结果差距很大:我们一步一步来分析一下:
首先来看一下View的setOnClickListener方法的源代码:
这个方法很简单就是设置监听器mOnClickListener,但是之前还有一段代码那就是setClick(true);从注释中可以看到,如果设置了点击监听器说明这个控件就是可以点击的了,再来看一下setClick方法:
这个方法就是设置一下可以点击的标志变量了,现在再来看一下onTouchEvent方法:
由于onTouchEvent方法的代码太长了,不要截图,但是大体意思是如果这个控件是可以点击的,那么onTouchEvent方法就返回true,当方法onTouchEvent方法返回true的时候说明这次事件被该控件消费了,不会再往上传递了,所以,我们给txt1添加onClick监听器的时候,运行结果中可以看到父控件的onTouch方法没有执行,当我们把onClick监听器删除的时候,父控件的onTouch方法执行了,这次的事件被父控件消费了,所以txt1的onTouch中的ACTION_UP中的代码就没有执行了。为了验证这一点其实很简单,你可以直接设置txt1.setClickable(false);这个方法通过设置false和true来观察结果;这里还有一个重要的信息是Android中像Button,CheckBox这样的控件默认都是可以点击的,所以不需要setClickable(true)方法来实现了,但是像TextView这样的控件默认是不可点击的,所以要通过setClickable(true)这样的方法来实现。
当然你也可以通过设置dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent这三个方法的返回值来观察结果,具体可以查看我的另外一篇文章:http://blog.csdn.net/jiangwei0910410003/article/details/16986039
至此,上述的两个问题就解决了,最大的收获还是学会了怎样去查找问题,最好的方式就是看源码!现在回到刚开始的地方,就是那个ListView为什么用setOnItemClick这个方法和onTouch这个方法有冲突:现在来想一想其实很简单了,因为如果onTouch方法中返回true的话,这次事件就被ListView中的item控件消费了,所以不会执行ListVIew的setOnItemClick这个方法了,如果onTouch方法返回false,那么会执行setOnItemClick方法,同时事件会被ListView消费了,所以onTouch方法只会执行ACTION_DOWN中的代码了,这里也说明了一点就是ListView的setOnItemClick方法和在getView中单独给item添加onClick方法的效果是不一样的。
同样还有另外的一个问题就是ListView中的Item中如果有Button,CheckBox等这样的组件的话,ListView中的setOnItemClick方法就是失效了,原因是Item没有获取焦点,焦点被Button等控件默认获取到了,这里有两种解决方法:
第一种就是让Button控件失去焦点,可以在布局文件中设置代码:android:focusable=”false”即可
第二种就是把setOnItemClick方法中的逻辑代码方法getView中的contentView的onClick方法中
为什么要这么做,有待研究呀!今天就写到这里了,头都写大了,很纠结,也很开心呀,如果发现有什么不正确的地方,希望给予批评和指正,本人将不胜感激!