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

前言

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

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

以上是 Android 中ListView setOnItemClickListener点击无效原因分析 的全部内容, 来源链接: utcz.com/z/330169.html

回到顶部