前言
最近在做项目的过程中,在使用listview的时候遇到了设置item监听事件的时候在没有回调onItemClick 方法的问题。我的情况是在item中有一个Button按钮。所以不会回调。上百度找到了解决办法有两种,如下:
1、在checkbox、button对应的view处加android:focusable=”false”
2、在item最外层添加属性 android:descendantFocusability=”blocksDescendants”
网上大多数帖子的理由是:当listview中包含button,checkbox等控件的时候,android会默认将focus给了这些控件,也就是说listview的item根本就获取不到focus,所以导致onitemclick时间不能触发。
由于自己想去验证一下,所有有了这篇文章。好了下面开始
我们为ListView设置的onItemClickListener是在何处回调的?
要搞清楚这个问题,我们先从 android事件分发机制开始说起,事件分发机制网上有大神写了一些特别详细和优秀的文章,在这里就只做简要介绍了:
事件分发重要的三个方法
该方法用来进行事件分发,在事件传递到当前View的时候调用,返回结果受到当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响。
该方法在上一个方法dispatchTouchEvent中调用,返回结果表示是否拦截当前事件,默认返回false,也就是不拦截。
在 dispatchTouchEvent方法中调用,该方法用来处理点击事件,返回结果表示是否消耗当前事件。
当点击事件触发之后的流程
了解事件分发机制之后,我们在setOnItemClick之后肯定需要进行事件处理,上面说到事件拦截默认是不拦截,所以我们猜想会到ListView的onTouchEvent方法中去处理ItemClick事件。去找你会发现ListView没有onTouchEvent方法。那我们再去他的父类AbsListView去找。还真有:
@Overridepublic 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 startedfinal View child = getChildAt(motionPosition - mFirstPosition);mMotionViewOriginalTop = child.getTop();mMotionPosition = motionPosition;}mLastY = y;break;}case MotionEvent.ACTION_POINTER_DOWN: {// New pointers take over dragging dutiesfinal 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 startedfinal 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() {@Overridepublic 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;@Overridepublic void run() {// The data has changed since we posted this action in the event queue,// bail out before bad things happenif (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 clickif (view != null) {performItemClick(view, motionPosition, adapter.getItemId(motionPosition));}}}}
第18行代码拿到了我们点击的item View,并且调用了performItemClick方法。我们再来看absListView的performItemClick方法:
@Overridepublic 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
@Overridepublic 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”
在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点击无效原因分析,希望大家喜欢。
新闻热点
疑难解答
图片精选