前言
最近在做项目的过程中,在使用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去找。还真有:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
@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);那么我们可以想到问题发生的原因应该就是在这个方法了里了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
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以及相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
@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方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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判断
1
2
3
4
5
6
|
if (inlist && !child.hasfocusable()) { if (mperformclick == null ) { mperformclick = new performclick(); } ..... } |
也就是只有item的view hasfocusable( )方法返回false,才会执行onitemclick。
view 和 viewgroup 的 hasfocusable
viewgroup的hasfocusable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@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
1
2
3
4
5
6
7
8
9
10
11
|
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也没有获取焦点,那么上面说到回调至性的条件判断也就的代码:
1
2
3
4
5
6
|
if (inlist && !child.hasfocusable()) { if (mperformclick == null ) { mperformclick = new performclick(); } ..... } |
if条件成立,所有执行了回调。
第二种情况,item,设置了descendantfocusability=”blocksdescendants”,所有没有遍历子 view,child.hasfocusable()直接返回false了。
以上所述是本文给大家分享的android 中listview setonitemclicklistener点击无效原因分析,希望大家喜欢。