欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-12-21 15:36:34
前言 最近在做项目的过程中,在使用listview的时候遇到了设置item监听事件的时候在没有回调onitemclick 方法的问题。我的情况是在item中有一个butt...

前言

最近在做项目的过程中,在使用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去找。还真有:

@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点击无效原因分析,希望大家喜欢。

上一篇:

下一篇: