Android 中ListView setOnItemClickListener点击无效原因分析
前言
最近在做项目的过程中,在使用listview的时候遇到了设置item监听事件的时候在没有回调onitemclick 方法的问题。我的情况是在item中有一个button按钮。所以不会回调。上百度找到了解决办法有两种,如下:
1、在checkbox、button对应的view处加android:focusable=”false”
android:clickable=”false” android:focusableintouchmode=”false”
2、在item最外层添加属性 android:descendantfocusability=”blocksdescendants”
网上大多数帖子的理由是:当listview中包含button,checkbox等控件的时候,android会默认将focus给了这些控件,也就是说listview的item根本就获取不到focus,所以导致onitemclick时间不能触发。
由于自己想去验证一下,所有有了这篇文章。好了下面开始
我们为listview设置的onitemclicklistener是在何处回调的?
要搞清楚这个问题,我们先从 android事件分发机制开始说起,事件分发机制网上有大神写了一些特别详细和优秀的文章,在这里就只做简要介绍了:
事件分发重要的三个方法
public boolean dispatchtouchevent(motionevent ev)
该方法用来进行事件分发,在事件传递到当前view的时候调用,返回结果受到当前view的ontouchevent和下级view的dispatchtouchevent方法的影响。
public boolean onintercepttouchevent(motionevent ev)
该方法在上一个方法dispatchtouchevent中调用,返回结果表示是否拦截当前事件,默认返回false,也就是不拦截。
public void ontouchevent(motionevent event)
在 dispatchtouchevent方法中调用,该方法用来处理点击事件,返回结果表示是否消耗当前事件。
当点击事件触发之后的流程
了解事件分发机制之后,我们在setonitemclick之后肯定需要进行事件处理,上面说到事件拦截默认是不拦截,所以我们猜想会到listview的ontouchevent方法中去处理itemclick事件。去找你会发现listview没有ontouchevent方法。那我们再去他的父类abslistview去找。还真有:
@override public boolean ontouchevent(motionevent ev) { if (!isenabled()) { // a disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return isclickable() || islongclickable(); } if (mpositionscroller != null) { mpositionscroller.stop(); } if (misdetaching || !isattachedtowindow()) { // something isn't right. // since we rely on being attached to get data set change notifications, // don't risk doing anything where we might try to resync and find things // in a bogus state. return false; } startnestedscroll(scroll_axis_vertical); if (mfastscroll != null && mfastscroll.ontouchevent(ev)) { return true; } initvelocitytrackerifnotexists(); final motionevent vtev = motionevent.obtain(ev); final int actionmasked = ev.getactionmasked(); if (actionmasked == motionevent.action_down) { mnestedyoffset = 0; } vtev.offsetlocation(0, mnestedyoffset); switch (actionmasked) { case motionevent.action_down: { ontouchdown(ev); break; } case motionevent.action_move: { ontouchmove(ev, vtev); break; } case motionevent.action_up: { ontouchup(ev); break; } case motionevent.action_cancel: { ontouchcancel(); break; } case motionevent.action_pointer_up: { onsecondarypointerup(ev); final int x = mmotionx; final int y = mmotiony; final int motionposition = pointtoposition(x, y); if (motionposition >= 0) { // remember where the motion event started final view child = getchildat(motionposition - mfirstposition); mmotionvieworiginaltop = child.gettop(); mmotionposition = motionposition; } mlasty = y; break; } case motionevent.action_pointer_down: { // new pointers take over dragging duties final int index = ev.getactionindex(); final int id = ev.getpointerid(index); final int x = (int) ev.getx(index); final int y = (int) ev.gety(index); mmotioncorrection = 0; mactivepointerid = id; mmotionx = x; mmotiony = y; final int motionposition = pointtoposition(x, y); if (motionposition >= 0) { // remember where the motion event started final view child = getchildat(motionposition - mfirstposition); mmotionvieworiginaltop = child.gettop(); mmotionposition = motionposition; } mlasty = y; break; } } if (mvelocitytracker != null) { mvelocitytracker.addmovement(vtev); } vtev.recycle(); return true; }
代码比较长,我们主要看46行 motionevent.action_up的情况,因为onitemclick事件的触发是在我们的手指从屏幕抬起的那一刻,在motionevent.action_up的情况下执行了ontouchup(ev);那么我们可以想到问题发生的原因应该就是在这个方法了里了。
private void ontouchup(motionevent ev) { switch (mtouchmode) { case touch_mode_down: case touch_mode_tap: case touch_mode_done_waiting: final int motionposition = mmotionposition; final view child = getchildat(motionposition - mfirstposition); if (child != null) { if (mtouchmode != touch_mode_down) { child.setpressed(false); } final float x = ev.getx(); final boolean inlist = x > mlistpadding.left && x < getwidth() - mlistpadding.right; if (inlist && !child.hasfocusable()) { if (mperformclick == null) { mperformclick = new performclick(); } final abslistview.performclick performclick = mperformclick; performclick.mclickmotionposition = motionposition; performclick.rememberwindowattachcount(); mresurrecttoposition = motionposition; if (mtouchmode == touch_mode_down || mtouchmode == touch_mode_tap) { removecallbacks(mtouchmode == touch_mode_down ? mpendingcheckfortap : mpendingcheckforlongpress); mlayoutmode = layout_normal; if (!mdatachanged && madapter.isenabled(motionposition)) { mtouchmode = touch_mode_tap; setselectedpositionint(mmotionposition); layoutchildren(); child.setpressed(true); positionselector(mmotionposition, child); setpressed(true); if (mselector != null) { drawable d = mselector.getcurrent(); if (d != null && d instanceof transitiondrawable) { ((transitiondrawable) d).resettransition(); } mselector.sethotspot(x, ev.gety()); } if (mtouchmodereset != null) { removecallbacks(mtouchmodereset); } mtouchmodereset = new runnable() { @override public void run() { mtouchmodereset = null; mtouchmode = touch_mode_rest; child.setpressed(false); setpressed(false); if (!mdatachanged && !misdetaching && isattachedtowindow()) { performclick.run(); } } }; postdelayed(mtouchmodereset, viewconfiguration.getpressedstateduration()); } else { mtouchmode = touch_mode_rest; updateselectorstate(); } return; } else if (!mdatachanged && madapter.isenabled(motionposition)) { performclick.run(); } } } mtouchmode = touch_mode_rest; updateselectorstate(); break; }
这里主要看7行到18行,拿到了我们item的view,并且在15行代码里判断了item的view是否在范围是否获取焦点(hasfocusable()),这里对hasfocusable()取反判断,也就是说,必需要我们的itemview的hasfocusable() 方法返回false, 才会执行一下的方法,以下的方法就是点击事件的方法。那么我们来看看是不是mperformclick真的就是执行我们的itemclick事件。
performclick以及相关代码如下:
private class performclick extends windowrunnnable implements runnable { int mclickmotionposition; @override public void run() { // the data has changed since we posted this action in the event queue, // bail out before bad things happen if (mdatachanged) return; final listadapter adapter = madapter; final int motionposition = mclickmotionposition; if (adapter != null && mitemcount > 0 && motionposition != invalid_position && motionposition < adapter.getcount() && samewindow()) { final view view = getchildat(motionposition - mfirstposition); // if there is no view, something bad happened (the view scrolled off the // screen, etc.) and we should cancel the click if (view != null) { performitemclick(view, motionposition, adapter.getitemid(motionposition)); } } } }
第18行代码拿到了我们点击的item view,并且调用了performitemclick方法。我们再来看abslistview的performitemclick方法:
@override public boolean performitemclick(view view, int position, long id) { boolean handled = false; boolean dispatchitemclick = true; if (mchoicemode != choice_mode_none) { handled = true; boolean checkedstatechanged = false; if (mchoicemode == choice_mode_multiple || (mchoicemode == choice_mode_multiple_modal && mchoiceactionmode != null)) { boolean checked = !mcheckstates.get(position, false); mcheckstates.put(position, checked); if (mcheckedidstates != null && madapter.hasstableids()) { if (checked) { mcheckedidstates.put(madapter.getitemid(position), position); } else { mcheckedidstates.delete(madapter.getitemid(position)); } } if (checked) { mcheckeditemcount++; } else { mcheckeditemcount--; } if (mchoiceactionmode != null) { mmultichoicemodecallback.onitemcheckedstatechanged(mchoiceactionmode, position, id, checked); dispatchitemclick = false; } checkedstatechanged = true; } else if (mchoicemode == choice_mode_single) { boolean checked = !mcheckstates.get(position, false); if (checked) { mcheckstates.clear(); mcheckstates.put(position, true); if (mcheckedidstates != null && madapter.hasstableids()) { mcheckedidstates.clear(); mcheckedidstates.put(madapter.getitemid(position), position); } mcheckeditemcount = 1; } else if (mcheckstates.size() == 0 || !mcheckstates.valueat(0)) { mcheckeditemcount = 0; } checkedstatechanged = true; } if (checkedstatechanged) { updateonscreencheckedviews(); } } if (dispatchitemclick) { handled |= super.performitemclick(view, position, id); } return handled; }
看第54行调用了父类的performitemclick方法:
public boolean performitemclick(view view, int position, long id) { final boolean result; if (monitemclicklistener != null) { playsoundeffect(soundeffectconstants.click); monitemclicklistener.onitemclick(this, view, position, id); result = true; } else { result = false; } if (view != null) { view.sendaccessibilityevent(accessibilityevent.type_view_clicked); } return result; }
好了,搞了半天,终于到点上了。第3
行代码很明显了,就是如果有itemclicklistener,就执行他的onitemclick方法,最终回调到我们常见的那个方法。
到这里,相信大家已经知道,关键代码就是刚才上面我们分析的那一个if判断
if (inlist && !child.hasfocusable()) { if (mperformclick == null) { mperformclick = new performclick(); } ..... }
也就是只有item的view hasfocusable( )方法返回false,才会执行onitemclick。
view 和 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) { 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; }
看源码我们可以知道:
如果 viewgroup visiable 和 focusable 都为 true,就算能够获取焦点, 返回 true。
如果我们给viewgroup设置了descendantfocusability属性,并且等于focus_block_descendants的情况下,返回false。不能获取焦点。
如果没有设置descendantfocusability属性的话,只要一个子view hasfocusable返回了true,viewgroup的hasfocusable就返回。
再来看view的hasfocusable
viewgroup的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 获取焦点,那么该 view 就不可能获取焦点
在触摸模式下如果不可获取焦点,并且没有父节点设置阻塞子 view 获取焦点,和在触摸模式下如果可以获取焦点,那么才判断 view 自身的 visiable 和 focusable 属性,来决定是否可以获取焦点,只有 visiable 和 focusable 同时为 true,该view 才可能获取焦点。
好了,分析到这里我们再回过头去看两个解决办法。
在checkbox、button对应的view处加android:focusable=”false”
android:clickable=”false” android:focusableintouchmode=”false”
在item最外层添加属性 android:descendantfocusability=”blocksdescendants”
第一种情况,item没有设置descendantfocusability=”blocksdescendants”,遍历了所有子view,由于所有的子view都不可获得焦点,所有item也没有获取焦点,那么上面说到回调至性的条件判断也就的代码:
if (inlist && !child.hasfocusable()) { if (mperformclick == null) { mperformclick = new performclick(); } ..... }
if条件成立,所有执行了回调。
第二种情况,item,设置了descendantfocusability=”blocksdescendants”,所有没有遍历子 view,child.hasfocusable()直接返回false了。
以上所述是本文给大家分享的android 中listview setonitemclicklistener点击无效原因分析,希望大家喜欢。