ListView setOnItemClickListener無效緣由分析

前言

最近在作項目的過程當中,在使用listview的時候遇到了設置item監聽事件的時候在沒有回調onItemClick 方法的問題。個人狀況是在item中有一個Button按鈕。因此不會回調。上百度找到了解決辦法有兩種,以下: 
一、在checkbox、button對應的view處加android:focusable=」false」 
android:clickable=」false」 android:focusableInTouchMode=」false」 
二、在item最外層添加屬性 android:descendantFocusability=」blocksDescendants」android

網上大多數帖子的理由是:當listview中包含button,checkbox等控件的時候,android會默認將focus給了這些控件,也就是說listview的item根本就獲取不到focus,因此致使onitemclick時間不能觸發app

因爲本身想去驗證一下,全部有了這篇文章。好了下面開始ide

咱們爲ListView設置的onItemClickListener是在何處回調的?

要搞清楚這個問題,咱們先從 android事件分發機制開始提及,事件分發機制網上有大神寫了一些特別詳細和優秀的文章,在這裏就只作簡要介紹了:post

事件分發重要的三個方法

public boolean dispatchTouchEvent(MotionEvent ev)

該方法用來進行事件分發,在事件傳遞到當前View的時候調用,返回結果受到當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響。this

public boolean onInterceptTouchEvent(MotionEvent ev)

該方法在上一個方法dispatchTouchEvent中調用,返回結果表示是否攔截當前事件,默認返回false,也就是不攔截。spa

public void onTouchEvent(MotionEvent event)

在 dispatchTouchEvent方法中調用,該方法用來處理點擊事件,返回結果表示是否消耗當前事件。code

當點擊事件觸發以後的流程

這裏寫圖片描述

瞭解事件分發機制以後,咱們在setOnItemClick以後確定須要進行事件處理,上面說到事件攔截默認是不攔截,因此咱們猜測會到ListView的onTouchEvent方法中去處理ItemClick事件。去找你會發現ListView沒有onTouchEvent方法。那咱們再去他的父類AbsListView去找。還真有:orm

 
@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);那麼咱們能夠想到問題發生的緣由應該就是在這個方法了裏了。blog

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了。

好了,分析到這裏相信你們已經很明白了。

若有對你有幫助,請各位大俠點下面的評論或點贊。若有錯誤請輕噴。。。。

相關文章
相關標籤/搜索