Android 中ListView setOnItemClickListener點擊無效原因分析
前言
最近在做項目的過程中,在使用listview的時候遇到了設(shè)置item監(jiān)聽事件的時候在沒有回調(diào)onItemClick 方法的問題。我的情況是在item中有一個Button按鈕。所以不會回調(diào)。上百度找到了解決辦法有兩種,如下:
1、在checkbox、button對應(yīng)的view處加android:focusable=”false”
android:clickable=”false” android:focusableInTouchMode=”false”
2、在item最外層添加屬性 android:descendantFocusability=”blocksDescendants”
網(wǎng)上大多數(shù)帖子的理由是:當listview中包含button,checkbox等控件的時候,android會默認將focus給了這些控件,也就是說listview的item根本就獲取不到focus,所以導(dǎo)致onitemclick時間不能觸發(fā)。
由于自己想去驗證一下,所有有了這篇文章。好了下面開始
我們?yōu)長istView設(shè)置的onItemClickListener是在何處回調(diào)的?
要搞清楚這個問題,我們先從 android事件分發(fā)機制開始說起,事件分發(fā)機制網(wǎng)上有大神寫了一些特別詳細和優(yōu)秀的文章,在這里就只做簡要介紹了:
事件分發(fā)重要的三個方法
public boolean dispatchTouchEvent(MotionEvent ev)
該方法用來進行事件分發(fā),在事件傳遞到當前View的時候調(diào)用,返回結(jié)果受到當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響。
public boolean onInterceptTouchEvent(MotionEvent ev)
該方法在上一個方法dispatchTouchEvent中調(diào)用,返回結(jié)果表示是否攔截當前事件,默認返回false,也就是不攔截。
public void onTouchEvent(MotionEvent event)
在 dispatchTouchEvent方法中調(diào)用,該方法用來處理點擊事件,返回結(jié)果表示是否消耗當前事件。
當點擊事件觸發(fā)之后的流程
了解事件分發(fā)機制之后,我們在setOnItemClick之后肯定需要進行事件處理,上面說到事件攔截默認是不攔截,所以我們猜想會到ListView的onTouchEvent方法中去處理ItemClick事件。去找你會發(fā)現(xiàn)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事件的觸發(fā)是在我們的手指從屏幕抬起的那一刻,在MotionEvent.ACTION_UP的情況下執(zhí)行了onTouchUp(ev);那么我們可以想到問題發(fā)生的原因應(yīng)該就是在這個方法了里了。
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, 才會執(zhí)行一下的方法,以下的方法就是點擊事件的方法。那么我們來看看是不是mPerformClick真的就是執(zhí)行我們的itemClick事件。
PerformClick以及相關(guān)代碼如下:
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,并且調(diào)用了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行調(diào)用了父類的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,就執(zhí)行他的onItemClick方法,最終回調(diào)到我們常見的那個方法。
到這里,相信大家已經(jīng)知道,關(guān)鍵代碼就是剛才上面我們分析的那一個if判斷
if (inList && !child.hasFocusable()) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } ..... }
也就是只有item的View hasFocusable( )方法返回false,才會執(zhí)行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設(shè)置了descendantFocusability屬性,并且等于FOCUS_BLOCK_DESCENDANTS的情況下,返回false。不能獲取焦點。
如果沒有設(shè)置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 的所有父節(jié)點,如果有一個父節(jié)點設(shè)置了阻塞子 View 獲取焦點,那么該 View 就不可能獲取焦點
在觸摸模式下如果不可獲取焦點,并且沒有父節(jié)點設(shè)置阻塞子 View 獲取焦點,和在觸摸模式下如果可以獲取焦點,那么才判斷 View 自身的 visiable 和 focusable 屬性,來決定是否可以獲取焦點,只有 visiable 和 focusable 同時為 true,該View 才可能獲取焦點。
好了,分析到這里我們再回過頭去看兩個解決辦法。
在checkbox、button對應(yīng)的view處加android:focusable=”false”
android:clickable=”false” android:focusableInTouchMode=”false”
在item最外層添加屬性 android:descendantFocusability=”blocksDescendants”
第一種情況,item沒有設(shè)置descendantFocusability=”blocksDescendants”,遍歷了所有子View,由于所有的子view都不可獲得焦點,所有item也沒有獲取焦點,那么上面說到回調(diào)至性的條件判斷也就的代碼:
if (inList && !child.hasFocusable()) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } ..... }
if條件成立,所有執(zhí)行了回調(diào)。
第二種情況,item,設(shè)置了descendantFocusability=”blocksDescendants”,所有沒有遍歷子 View,child.hasFocusable()直接返回false了。
以上所述是本文給大家分享的Android 中ListView setOnItemClickListener點擊無效原因分析,希望大家喜歡。
- Android ListView的item中嵌套ScrollView的解決辦法
- Android中Listview點擊item不變顏色及設(shè)置listselector 無效的解決方案
- Android 中ListView的Item點擊事件失效的快速解決方法
- Android ListView的OnItemClickListener詳解
- Android使用ListView批量刪除item的方法
- Android實現(xiàn)帶有邊框的ListView和item的方法
- Android中ListView Item布局優(yōu)化技巧
- Android開發(fā)之ListView實現(xiàn)Item局部刷新
- Android ListView的item背景色設(shè)置和item點擊無響應(yīng)的解決方法
- Android中ListView的item點擊沒有反應(yīng)的解決方法
相關(guān)文章
android RecycleView實現(xiàn)下拉刷新和上拉加載
這篇文章主要為大家詳細介紹了android RecycleView實現(xiàn)下拉刷新和上拉加載,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-06-06Android自定義控件實現(xiàn)可左右滑動的導(dǎo)航條
這篇文章主要介紹了Android自定義控件實現(xiàn)可左右滑動的導(dǎo)航條,能響應(yīng)快速滑動,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-07-07Android中AsyncTask與handler用法實例分析
這篇文章主要介紹了Android中AsyncTask與handler用法,以實例形式較為詳細的分析了Android中AsyncTask與handler的功能、用法與相關(guān)注意事項,并附帶完整實例源碼供讀者下載,需要的朋友可以參考下2015-10-10Android開發(fā)之Fragment懶加載的幾種方式及性能對比
這篇文章主要介紹了Android開發(fā)之Fragment懶加載的幾種方式及性能對比的相關(guān)資料,具體詳細介紹需要的小伙伴可以參考下面文章內(nèi)容2022-05-05Android 讀取assets和raw文件內(nèi)容實例代碼
這篇文章主要介紹了Android 讀取assets和raw文件內(nèi)容的相關(guān)資料,并附簡單實例代碼,需要的朋友可以參考下2016-10-10Android編程實現(xiàn)通知欄進度條效果的方法示例
這篇文章主要介紹了Android編程實現(xiàn)通知欄進度條效果的方法,結(jié)合實例形式較為詳細的分析了Android通知欄進度條效果的功能、布局相關(guān)實現(xiàn)方法與操作注意事項,需要的朋友可以參考下2018-02-02Android基于zxing的二維碼(網(wǎng)格)掃描 仿支付寶網(wǎng)格掃描
這篇文章主要為大家詳細介紹了Android基于zxing的二維碼網(wǎng)格掃描,仿支付寶網(wǎng)格掃描,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03