转至:http://www.jb51.net/article/77792.htm
前言
最近在做项目的过程中,在使用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点击无效原因分析,希望大家喜欢。