前言

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

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

ListView setOnItemClickListener无效原因分析_ide

了解事件分发机制之后,我们在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;
}


看源代码我们能够知道:

  1. 假设 ViewGroup visiable 和 focusable 都为 true,就算能够获取焦点, 返回 true。
  2. 假设我们给ViewGroup设置了descendantFocusability属性。而且等于FOCUS_BLOCK_DESCENDANTS的情况下。返回false。不能获取焦点。
  3. 假设没有设置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();
}


  1. 在触摸模式下假设不可获取焦点,先遍历 View 的全部父节点,假设有一个父节点设置了堵塞子 View 获取焦点,那么该 View 就不可能获取焦点
  2. 在触摸模式下假设不可获取焦点,而且没有父节点设置堵塞子 View 获取焦点。和在触摸模式下假设能够获取焦点,那么才推断 View 自身的 visiable 和 focusable 属性,来决定能否够获取焦点,仅仅有 visiable 和 focusable 同一时候为 true。该View 才可能获取焦点。

好了,分析到这里我们再回过头去看两个解决的方法。

  1. 在checkbox、button相应的view处加android:focusable=”false”
    android:clickable=”false” android:focusableInTouchMode=”false”
  2. 在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了。

好了,分析到这里相信大家已经非常明确了。

如有对你有帮助。请各位大侠点以下的评论或点赞。如有错误请轻喷。。。。