zoukankan      html  css  js  c++  java
  • 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;
        }

    看源码我们可以知道:

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

    好了,分析到这里相信大家已经很明白了。

    如有对你有帮助,请各位大侠点下面的评论或点赞。如有错误请轻喷。。。。

  • 相关阅读:
    新年后的第一个学习总结
    2021/02/07周学习总结
    内网穿透
    有效的括号
    实现一个简单的模板字符串替换
    二叉树的最大深度
    前端性能和错误监控
    前端缓存
    display: none; opacity: 0; visibility: hidden;
    发布订阅模式与观察者模式
  • 原文地址:https://www.cnblogs.com/yangqiangyu/p/5112941.html
Copyright © 2011-2022 走看看