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

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

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要实现一个新闻页面,你需要以下步骤: 1. 创建一个布局文件,用于显示新闻列表。可以使用 RecyclerView 来显示新闻列表。 2. 创建一个适配器类,用于将新闻数据绑定到 RecyclerView 上。适配器类需要继承 RecyclerView.Adapter 类,并实现 onCreateViewHolder 和 onBindViewHolder 方法。 3. 创建一个数据模型类,用于存储新闻数据。数据模型类应该包含新闻标题、新闻内容、新闻图片等信息。 4. 在 Activity 或 Fragment 中获取新闻数据,并将数据传递给适配器。 5. 在适配器中实现数据绑定逻辑,将新闻数据显示到 RecyclerView 上。 6. 添加点击事件处理逻辑,当用户点击某个新闻时,打开一个新的 Activity 或 Fragment 显示新闻详情。 7. 可以根据需求添加其他功能,比如下拉刷新、上拉加载更多等。 下面是一个简单的示例代码: 1. 布局文件 news_list_item.xml ``` <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:id="@+id/news_image" android:layout_width="match_parent" android:layout_height="wrap_content" android:adjustViewBounds="true" android:scaleType="centerCrop" android:src="@drawable/news_placeholder" /> <TextView android:id="@+id/news_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="18sp" android:textStyle="bold" android:padding="8dp" /> <TextView android:id="@+id/news_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="16sp" android:padding="8dp" /> </LinearLayout> ``` 2. 适配器类 NewsListAdapter.java ``` public class NewsListAdapter extends RecyclerView.Adapter<NewsListAdapter.ViewHolder> { private List<News> mNewsList; public NewsListAdapter(List<News> newsList) { mNewsList = newsList; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_list_item, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { News news = mNewsList.get(position); holder.newsTitle.setText(news.getTitle()); holder.newsContent.setText(news.getContent()); holder.newsImage.setImageResource(news.getImageResId()); } @Override public int getItemCount() { return mNewsList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { ImageView newsImage; TextView newsTitle; TextView newsContent; ViewHolder(View itemView) { super(itemView); newsImage = itemView.findViewById(R.id.news_image); newsTitle = itemView.findViewById(R.id.news_title); newsContent = itemView.findViewById(R.id.news_content); } } } ``` 3. 数据模型类 News.java ``` public class News { private String mTitle; private String mContent; private int mImageResId; public News(String title, String content, int imageResId) { mTitle = title; mContent = content; mImageResId = imageResId; } public String getTitle() { return mTitle; } public String getContent() { return mContent; } public int getImageResId() { return mImageResId; } } ``` 4. Activity 中获取新闻数据并显示到 RecyclerView 上 ``` public class NewsActivity extends AppCompatActivity { private RecyclerView mRecyclerView; private NewsListAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_news); // 获取新闻数据 List<News> newsList = getNewsList(); // 初始化 RecyclerView mRecyclerView = findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mAdapter = new NewsListAdapter(newsList); mRecyclerView.setAdapter(mAdapter); } private List<News> getNewsList() { // 从网络或本地数据库获取新闻数据 // ... // 这里只是模拟数据 List<News> newsList = new ArrayList<>(); newsList.add(new News("新闻标题1", "新闻内容1", R.drawable.news_image1)); newsList.add(new News("新闻标题2", "新闻内容2", R.drawable.news_image2)); newsList.add(new News("新闻标题3", "新闻内容3", R.drawable.news_image3)); return newsList; } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值