##前言
之前在项目中遇到这么个问题,对ListView进行如下设置,发现点击事件并未响应,由于项目进度紧,所以采用另一方法来解决这一问题,另一方法在文章最后面会贴上,觉得文章篇幅太长的话,可直接看文章最后面的总结
ListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//
}
});
前几天一朋友也遇到这问题,趁着有空赶忙探究下,可能现在大多项目都用recyclerview控件,在项目中我也用到recyclerview控件,但ListView还是有必要探讨下
##开始
那么首先通过重写ListView控件,重写其事件分发、事件拦截,事件处理等方法,打印返回值,下面是重写的代码:
/**
* Created by WYK on 2016/10/31.
*/
public class MyListViewview extends ListView{
public MyListViewview(Context context) {
super(context);
}
public MyListViewview(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListViewview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
LogUtil.logi("------dispatchTouchEvent------------->");
boolean b = super.dispatchTouchEvent(ev);
LogUtil.logi("------dispatchTouchEvent------------->" + b);
return b;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
LogUtil.logi("------onInterceptTouchEvent------------->");
boolean b = super.onInterceptTouchEvent(ev);
LogUtil.logi("------onInterceptTouchEvent------------->" + b);
return b;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
LogUtil.logi("------onTouchEvent----------------------->");
boolean b = super.onTouchEvent(ev);
LogUtil.logi("------onTouchEvent----------------------->" + b);
return super.onTouchEvent(ev);
}
}
重写ListView之后,将xml布局中的ListView控件替换成重写的MyListViewview,下面是简化后的布局代码:
<?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:background="@color/color_white">
<com.wyk.kk.view.SideBar
android:id="@+id/sidebar"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_alignParentRight="true"/>
<com.wyk.kk.version.five.view.MyListViewview
android:id="@+id/listview"
android:layout_toLeftOf="@id/sidebar"
android:listSelector="@android:color/transparent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:divider="@null"/>
<TextView
android:id="@+id/tv_peoplehub_myfriend_sidebar_dialog"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@drawable/shape_dialog_sidebar"
android:layout_centerInParent="true"
android:gravity="center"
android:text="A"
android:textColor="@android:color/white"
android:textSize="@dimen/textsize_36"
android:visibility="gone" />
</RelativeLayout>
那就运行起来后点击条目,日志打印如下:
328 13413-13413/com.wyk.kk I/info======>: ---dispatchTouchEvent--->
328 13413-13413/com.wyk.kk I/info======>: ---onInterceptTouchEvent--->
329 13413-13413/com.wyk.kk I/info======>: ---onInterceptTouchEvent--->false
329 13413-13413/com.wyk.kk I/info======>: ---dispatchTouchEvent--->true
408 13413-13413/com.wyk.kk I/info======>: ---dispatchTouchEvent--->
408 13413-13413/com.wyk.kk I/info======>: ---onInterceptTouchEvent--->
408 13413-13413/com.wyk.kk I/info======>: ---onInterceptTouchEvent--->false
409 13413-13413/com.wyk.kk I/info======>: ---dispatchTouchEvent--->true
点击事件并未响应,可以看到dispatchTouchEvent的返回值为true(备注:文章最后面会贴上关于事件分发机制的牛逼文章),则说明事件已经被消费了,那么到底是谁消费事件呢?回头来看看ListView条目的布局
<?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="wrap_content">
<RelativeLayout
android:id="@+id/rl_people_hub_myfriend_tag"
android:layout_width="match_parent"
android:layout_height="28dp"
android:visibility="gone">
<TextView
android:id="@+id/tv_item_people_hub_myfriend_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:paddingLeft="10dip"
android:textColor="@color/color_blacker"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.3dp"
android:layout_alignParentBottom="true"
android:background="@color/item_line_cover" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_my_friend_item"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_below="@id/rl_people_hub_myfriend_tag"
android:background="@drawable/selector_header_back"
android:clickable="true"> //这里设置了clickable
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/iv_people_hub_myfriend_ico"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:src="@drawable/yh_usercenter_editdata_photo" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_toRightOf="@id/iv_people_hub_myfriend_ico">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_people_hub_myfriend_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="4dip"
android:width="100dp"
android:singleLine="true"
android:textColor="@color/text_gray"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_friend_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="4dip"
android:width="100dp"
android:singleLine="true"
android:textColor="@color/text_gray_01"
android:textSize="14sp" />
</LinearLayout>
<CheckBox
android:id="@+id/checkbox_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="30dp"
android:visibility="gone"/> //note
<View
android:layout_width="match_parent"
android:layout_height="0.3dp"
android:layout_alignParentBottom="true"
android:background="@color/item_line_cover" />
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
上面ListView的条目布局中出现CheckBox这控件和RelativeLayout布局中存在属性android:clickable=“true” ,那么会不会是这两者抢走了事件?排除法对其进行探究之后发现:
- 1.当将CheckBox控件从布局移除之后,ListView事件仍无响应;
- 2.将CheckBox移除,从RelativeLayout中删除属性clickable,ListView事件有响应;
- 3.从RelativeLayout删除属性clickable,并将CheckBox设置为显示状态,ListView事件仍无响应。
- 4.从RelativeLayout中删除属性clickable,而CheckBox是处于Gone状态的,ListView事件有响应;
上面简单描述了可能影响ListView事件无响应测试的结果,那么接下来我们就总结所分析的:
1.在上面的条目布局中,CheckBox并未抢了ListView的事件响应,主要由于在布局中将其设置为android:visibility=“gone”;
2.如果我们将CheckBox设置为显示状态,并删除RelativeLayout里的属性clickable,则ListView事件仍无响应,那么改下MyListView里重写的onTouchEvent方法实现:
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean b = false;
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:
LogUtil.logi("onTouchEvent---down->");
b = super.onTouchEvent(ev);
LogUtil.logi("onTouchEvent---down->" + b);
break;
case MotionEvent.ACTION_MOVE:
LogUtil.logi("onTouchEvent---move->");
b = super.onTouchEvent(ev);
LogUtil.logi("onTouchEvent---move->" + b);
break;
case MotionEvent.ACTION_UP:
LogUtil.logi("onTouchEvent---up->");
b = super.onTouchEvent(ev);
LogUtil.logi("onTouchEvent---up->" + b);
break;
}
return b;
}
日志打印如下(ListView事件是无响应的情况),可以看到下面的onTouchEvent结果是返回true,分别对应事件的按下/触摸/抬起这三个动作;
280 25188-25188/com.wyk.kk I/info======>: dispatchTouchEvent---->
280 25188-25188/com.wyk.kk I/info======>: onInterceptTouchEvent---->
281 25188-25188/com.wyk.kk I/info======>: onInterceptTouchEvent---->false
285 25188-25188/com.wyk.kk I/info======>: onTouchEvent---down->
286 25188-25188/com.wyk.kk I/info======>: onTouchEvent---down->true
286 25188-25188/com.wyk.kk I/info======>: dispatchTouchEvent---->true
325 25188-25188/com.wyk.kk I/info======>: dispatchTouchEvent---->
325 25188-25188/com.wyk.kk I/info======>: onTouchEvent---move->
326 25188-25188/com.wyk.kk I/info======>: onTouchEvent---move->true
326 25188-25188/com.wyk.kk I/info======>: dispatchTouchEvent---->true
341 25188-25188/com.wyk.kk I/info======>: dispatchTouchEvent---->
342 25188-25188/com.wyk.kk I/info======>: onTouchEvent---move->
342 25188-25188/com.wyk.kk I/info======>: onTouchEvent---move->true
342 25188-25188/com.wyk.kk I/info======>: dispatchTouchEvent---->true
352 25188-25188/com.wyk.kk I/info======>: dispatchTouchEvent---->
352 25188-25188/com.wyk.kk I/info======>: onTouchEvent---up->
353 25188-25188/com.wyk.kk I/info======>: onTouchEvent---up->true
353 25188-25188/com.wyk.kk I/info======>: dispatchTouchEvent---->true
如果在CheckBox加属性"android:focusable=“false”,运行之后点击是有响应事件的,日志打印结果跟上面的一样。之所以刚才条目无响应,可能是由于CheckBox捕获了焦点;当checkBox设置为隐藏的状态(Gone、invisible),则点击事件还是交给listView去处理的,这又是为何? 个人猜测的原因是:"控件都处于隐藏的状态,那捕获焦点还能做什么? "(补充:接下来的分析中会看到在AbsListView的onTouchUp方法会进行条目的子View焦点判断,最终会调用到View的hasFocusable方法,可以在hasFocusable方法里看到,只要View的状态不是VISIBLE,View的hasFocusable方法则返回false),看看View源码的hasFocusable方法的实现如下:
public boolean hasFocusable() {
if (!isFocusableInTouchMode()) {
for (ViewParent p = mParent; p instanceof ViewGroup; p = p.getParent()) {
final ViewGroup g = (ViewGroup) p;
if (g.shouldBlockFocusForTouchscreen()) {
return false;
}
}
}
return (mViewFlags & VISIBILITY_MASK) == VISIBLE && isFocusable();
}
可以看到是先遍历View的父节点,只要有一父节点阻塞了焦点,该子View就获取不到焦点,返回false;如果父节点并没有阻塞焦点,最后还要根据View的显示状态和焦点状态进行判断;上面的CheckBox控件捕获焦点和这里View的焦点是否存在的判断 貌似没有关联,其实不是的!ListView继承自AbsListView,再想想事件响应一般都是在onTouchEvent方法里,那么看看AbsListView的onTouchEvent方法 (为什么不看ListView的onTouchEvent方法? 因为ListView并没有重写onTouchEvent方法),而条目响应是在手指抬起的时候,那么就可以看看onTouchUp这个方法,因为onTouchUp是在onTouchEvent响应手指抬起事件的子方法,可以看到在这里面有这么一段代码:
private void onTouchUp(MotionEvent ev) {
//这里的child == ListView某条条目
final View child = getChildAt(motionPosition - mFirstPosition);
//省略
final float x = ev.getX();
final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
//条目进行范围判断以及该条目的子View焦点的判断
if (inList && !child.hasFocusable()) { //note
if(mPerformClick == null) {
mPerformClick = new PerformClick();//条目事件响应逻辑
}
final AbsListView.PerformClick performClick = mPerformClick;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
//省略
在上面的小段源码中,在注释"note"的该行代码,可以看到需要对条目进行范围判断以及item的子View焦点的判断,而这里的焦点判断就显得很重要了,只有当条目的全部子View当前的焦点都为false时,才会进行条目事件的响应(看下面ViewGroup的hasFocusable的实现),所以item里的任意子View的焦点必须为false,才能进入该方法,条目事件才可以得到响应。再回来看CheckBox,由于加属性"android:focusable="false"之后,ListView是可以响应事件的,这样就解决 “ListView由于条目的布局存在CheckBox控件导致条目点击无响应的问题”;
###第二种解决方法:
将ListView条目的根布局(必须为ViewGroup)加上属性
android:descendantFocusability="blocksDescendants"
###顺便学习下descendantFocusability属性
该属性主要用于解决ViewGroup和其子控件焦点优先级的问题,这个属性的存在,就很方便解决上面ListView与CheckBox事件的问题。
属性的值有三种:
beforeDescendants:viewgroup会优先其子类控件而获取到焦点
afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点
探究下android:descendantFocusability="blocksDescendants"的作用点,先看看下面ViewGroup的hasFocusable的方法:
//ViewGroup的hasFocusable
@Override
public boolean hasFocusable() {
if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
if (isFocusable()) {
return true;
}
final int descendantFocusability = getDescendantFocusability();
if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { //note
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
if (child.hasFocusable()) {
return true;
}
}
}
return false;
}
在上面 标注 "note"的那行代码,可以看到如果ViewGroup有"blocksDescendants"属性值作用的时候,ViewGroup的hasFocusable直接就返回false,而AbsListView类的onTouchUp方法里的mPerformClick就可以被执行,条目事件也就可以得到响应咯。这里还可以看到关于ViewGroup和View关于焦点的判断流程,从ViewGroup的hasFocusable方法看到当显示状态和焦点存在满足的条件下,会进行ViewGroup的子View的焦点的判断,只要有一个子View焦点返回true,则ViewGroup的焦点直接返回true;
3.探究完CheckBox的问题,接着看看 “布局由于控件的clickable属性的存在 而导致事件并未响应的问题”
其实布局设置里有控件设置clickable属性值为true,则表示其能处理Touch事件,看下面
View的onTouchEvent方法的部分源码
public boolean onTouchEvent(MotionEvent event) {
//省略
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//省略
}
看了上面源码,是不是可以理解咯,当设置clickable/long_clickable/context_clickable 任一属性,则View在抬起的事件直接返回true,表示其能消费,则事件响应在该View,导致条目点击是无响应的;
在这里记录另外一种解决ListView条目点击未响应的解决方法
其实ListView的条目布局不都是有一个根控件,比如上面的RelativeLayout根布局,在ListView的适配器BaseAdapter的getView方法不是要将布局inflate之后再寻找布局里的控件,为其子View设置数据以及交互,那么拿到RelativeLayout根布局也是可以的,只要为它设置id,find到之后只要设置点击事件及实现交互逻辑,这样即可。
感觉思路是不是挺乱的,总结下
1.ListView的条目布局如果有CheckBox、Button这些控件,可以设置其焦点为false,也可以为根布局设置属性descendantFocusability,属性值为blocksDescendants,这样子条目点击事件就可以得到响应咯
2.ListView的条目布局里的控件不可设置Clickable为true的属性,否则事件会被该控件以do nothing的方式消费。
3.简单总结下ListView条目响应的大致回调流程
类 方法
AbsListView
onTouchEvent
onTouchUp
------重点在:child.hasFocusable()
AbsListView内部类PerformClick
performItemClick
AdapterView
performItemClick
----- mOnItemClickListener.onItemClick(this, view, position, id);事件响应
//----------------------------------------------------------------
顺便看下ListView的继承关系
ListView ----》 AbsListView ----》AdapterView ----》ViewGroup ----》View
参考博文
- 事件分发机制:http://www.jianshu.com/p/e99b5e8bd67b
- ListView问题探讨:http://www.jb51.net/article/77792.htm
- Touch事件处理 :http://www.cnblogs.com/xiaoweiz/p/3838682.html