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

##前言
之前在项目中遇到这么个问题,对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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值