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

    好了,分析到这里相信大家已经非常明确了。

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

  • 相关阅读:
    关于报错”已有打开的于此Command相关联的DataReader,必须首先将它关闭。“的问题
    Dockerjenkins环境搭建跟配置
    Python+selenium 【第十章】封装config类,读取配置文件,封装驱动类,适配windows/mac
    —— “欢迎来到我的小屋”
    Python+selenium 【第九章】封装excel类,封装test_data类 读取测试元素信息
    Linux搭建docker
    Python+selenium 【第十二章】封装日志类优化其他方法并添加日志(异常处理)
    【selenium】Python自动化面试题
    Dockerjenkins容器安装python并配置环境变量
    【优化框架】新增期望结果,断言为mysql情况下动态化参数替换
  • 原文地址:https://www.cnblogs.com/wzjhoutai/p/7169309.html
Copyright © 2020-2023  润新知