Android 中ListView setOnItemClickListener点击无效原因分析

时间:2022-09-20 16:17:21

前言

最近在做项目的过程中,在使用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方法中调用,该方法用来处理点击事件,返回结果表示是否消耗当前事件。

当点击事件触发之后的流程

Android 中ListView setOnItemClickListener点击无效原因分析

了解事件分发机制之后,我们在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点击无效原因分析,希望大家喜欢。