前言
近期在做项目的过程中,在使用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了。
好了,分析到这里相信大家已经非常明确了。
如有对你有帮助。请各位大侠点以下的评论或点赞。如有错误请轻喷。。。。