您的位置:首页 > 移动开发 > Android开发

Android 控件ListView 条目响应深入探索

2016-11-02 12:40 330 查看

前言

之前在项目中遇到这么个问题,对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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐