OnItemClickListener 没响应的根本原因-源码解析

如下一个简单示例

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private ListView myList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        myList = (ListView) findViewById(R.id.list);

        myList.setAdapter(new MyListAdapter(this));

        myList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//                Log.d(TAG, "MainActivity");
                // 通过这个打印出调用栈
                Exception exception = new Exception();
                StackTraceElement[] stes = exception.getStackTrace();
                for (StackTraceElement s: stes) {
                    Log.d(TAG, s.getClassName() + "==" + s.getMethodName() + "=="+s.getLineNumber());
                }
            }
        });
    }

    private class MyListAdapter extends ArrayAdapter<String> {

        public MyListAdapter(Context context) {
            super(context, R.layout.item_list, new ArrayList<String>());
        }

        @Override
        public int getCount() {
            return 5;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return LayoutInflater.from(MainActivity.this).inflate(R.layout.item_list, parent, false);
        }
    }
}
对应的item_list为

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="70dp">

    <!--<Button-->
        <!--android:text="button"-->
        <!--android:layout_width="100dp"-->
        <!--android:layout_height="50dp" />-->
</LinearLayout>
因为如果加上Button后,上面的
OnItemClickListener
将没有被调用,为了查看具体的调用过程,方便分析,所以暂时把Button注释;(需要注意的是为了简便,没有使用ViewHolder提升性能)。

点击一个item后打印出如下信息

www.seven.com.logdemo.MainActivity$1==onItemClick==39   //最后调用的是我们的匿名内部类的onItemClick 在MainActivity中
android.widget.AdapterView==performItemClick==298      // AdapterView 的performItemClick函数
android.widget.AbsListView==performItemClick==1086    // AbsListView 的 performItemClick函数
android.widget.AbsListView$PerformClick==run==2855    // AbsListView 的内部类PerformClick 的 run函数, (可能是一个Runnable)
android.widget.AbsListView$1==run==3529    // AbsListView的一个匿名内部类 的run 函数 (匿名内部类可能是一个Runable)
android.os.Handler==handleCallback==615   // 自此下面的不用再管。这是Handler的消息循环回调
android.os.Handler==dispatchMessage==92
android.os.Looper==loop==137
android.app.ActivityThread==main==4745
java.lang.reflect.Method==invokeNative==-2
java.lang.reflect.Method==invoke==511
com.android.internal.os.ZygoteInit$MethodAndArgsCaller==run==786
com.android.internal.os.ZygoteInit==main==553
dalvik.system.NativeStart==main==-2

因为行号并不准确,所以我们需要慢慢寻找对应的函数。 

1. AdapterView 的 performItemClick 

    /**
     * Call the OnItemClickListener, if it is defined. Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @param view The view within the AdapterView that was clicked.
     * @param position The position of the view in the adapter.
     * @param id The row id of the item that was clicked.
     * @return True if there was an assigned OnItemClickListener that was
     *         called, false otherwise is returned.
     */
    public boolean performItemClick(View view, int position, long id) {
        final boolean result;
        if (mOnItemClickListener != null) { // 由于我们设置的就是mOnItemClickListener,所以这个条件必定成立, 因此这个函数没问题
            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;
    }

2.AbsListView(继承AdapterView) 的 performItemClick

    public boolean performItemClick(View view, int position, long id) {
        boolean handled = false;
        boolean dispatchItemClick = true;

        if (mChoiceMode != CHOICE_MODE_NONE) { // 查找mChoiceMode的定义,发现 int mChoiceMode = CHOICE_MODE_NONE;  默认情况下这个条件不成立
            handled = true;
            boolean checkedStateChanged = false;

            if (mChoiceMode == CHOICE_MODE_MULTIPLE ||
                    (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) {
                ...
                if (mChoiceActionMode != null) {
                    
                    dispatchItemClick = false;
                }
                checkedStateChanged = true;
            } else if (mChoiceMode == CHOICE_MODE_SINGLE) {
                ...
            }
        }

        // 所以 这个条件必定成立,接着就是调用AdapterView的performItemClick, 也就是说这个函数也没有问题
        if (dispatchItemClick) {
            handled |= super.performItemClick(view, position, id);
        }

        return handled;
    }

3. AbsListView的内部类PerformClick 的run函数。

    private class WindowRunnnable {
        private int mOriginalAttachCount;

        public void rememberWindowAttachCount() {
            mOriginalAttachCount = getWindowAttachCount();
        }

        // 正常情况下 这个都是true 因为是用 rememberWindowAttachCount() 来获取 mOriginalAttachCount 的值的
        public boolean sameWindow() {
            return getWindowAttachCount() == mOriginalAttachCount;
        }
    }

    private class PerformClick extends WindowRunnnable implements Runnable { // 果然是一个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 &&  // adapter不为空, mItemCount肯定大于0, motionPosition 点击的位置也是有效的 同时也是小于adapter.getCount() sameWindow也是true(上面解析) ,所以这个函数也没问题,
                                                      // 即不会因为item_list布局加一个Button控件导致此条件语句不成立。
                    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) {
                    // 取到对应点击的item view后,就执行了performItemClick
                    performItemClick(view, motionPosition, adapter.getItemId(motionPosition));
                }
            }
        }
    }
4.AbsListView匿名内部类的run函数。

这个run函数比较难找,但是可以根据上面的信息查找; 发现 AbsListView有个 mPerformClick ;所以匿名的内部类的run函数,必定是调用了mPerformClick 的run函数

    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();
                    // 如果点击的位置的x坐标在listView中,那么inList为true (明显是true)
                    final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
                    if (inList && !child.hasFocusable()) { // 发现目标!!!!!, 想要进入下面,将Runnable加入到消息队列中,那么被点击的Child view 的hasFocusable()必须返回false
                                                           // 到底什么情况下hasFocusable()返回false呢,进入View内查看 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) {
                            ...
                            if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                                ...
                                
                                // 上面调用栈的匿名内部Runnable类
                                mTouchModeReset = new Runnable() {
                                    @Override
                                    public void run() {
                                        mTouchModeReset = null;
                                        mTouchMode = TOUCH_MODE_REST;
                                        child.setPressed(false);
                                        setPressed(false);
                                        if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
                                            // 调用了 performClick 的run函数
                                            performClick.run();
                                        }
                                    }
                                };
                                
                                // 在这里将其加入到Handler的消息循环队列中
                                postDelayed(mTouchModeReset,
                                        ViewConfiguration.getPressedStateDuration());
                            } else {
                                
                            }
                            return;
                        } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                            performClick.run();
                        }
                    }
                }
                。。。
                break;
            case TOUCH_MODE_SCROLL:
                。。。

            case TOUCH_MODE_OVERSCROLL:
                。。。

                break;
        }

    }

到底什么情况下hasFocusable()返回false呢,进入View内查看 hasFocusable()
5. View 的 hasFocusable()函数如下

/**
     * 如果该view是focusable的 或者 该view的子view中存在一个是focusable的,那么这个函数返回true
     * 这里的注释已经将viewGroup的 hasFocusable() 也包含在内了
     *
     *
     * Returns true if this view is focusable or if it contains a reachable View
     * for which {@link #hasFocusable()} returns true. A "reachable hasFocusable()"
     * is a View whose parents do not block descendants focus.
     *
     * Only {@link #VISIBLE} views are considered focusable.
     *
     * @return True if the view is focusable or if the view contains a focusable
     *         View, false otherwise.
     *
     * @see ViewGroup#FOCUS_BLOCK_DESCENDANTS
     * @see ViewGroup#getTouchscreenBlocksFocus()
     */
    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;
                }
            }
        }
        // 如果是可见的 可聚焦的 那么返回true
        return (mViewFlags & VISIBILITY_MASK) == VISIBLE && isFocusable();
    }


    // 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) { // 当ViewGroup的descendantFocusability为FOCUS_BLOCK_DESCENDANTS时,这个条件不成立,因此返回false
            final int count = mChildrenCount;
            final View[] children = mChildren;

            for (int i = 0; i < count; i++) { // 只要有一个child的 hasFocusable() 返回true, 那么ViewGroup 的hasFocusable()返回true
                final View child = children[i];
                if (child.hasFocusable()) {
                    return true;
                }
            }
        }

        return false;
    }

hasFocusable()的结果依赖于 isFocusable(),  isFocusable()可以通过在布局中设置 

android:focusable="false"
来改变对应的结果。

    @ViewDebug.ExportedProperty(category = "focus")
    public final boolean isFocusable() {
        return FOCUSABLE == (mViewFlags & FOCUSABLE_MASK);
    }



根据上面的分析

下面是往ListView的Item里面添加了Button等能够获取焦点的控件,从而导致OnItemClickListener.onItemClick不会被执行的解决办法。


a.如果listView的itemView是ViewGroup,那么就需要itemView.hasFocusable()返回false , 对应的OnItemClickListener.onItemClick才会被执行,

可以通过继承对应的ViewGroup,重写hasFocusable()函数,默认返回false 例如

public class NoFocusLinearLayout extends LinearLayout {
    public NoFocusLinearLayout(Context context) {
        super(context);
    }

    public NoFocusLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NoFocusLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public NoFocusLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean hasFocusable() {
        return false;
    }
}
对应的布局文件

<?xml version="1.0" encoding="utf-8"?>
<www.seven.com.logdemo.NoFocusLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="70dp">

    <Button
        android:text="button"
        android:layout_width="100dp"
        android:layout_height="50dp" />
</www.seven.com.logdemo.NoFocusLinearLayout>



b.如果listView的itemView是ViewGroup,那么也可以通过设置ViewGroup 的 descendantFocusability 为 FOCUS_BLOCK_DESCENDANTS ,对应的OnItemClickListener.onItemClick也会被执

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:descendantFocusability="blocksDescendants" // 添加的属性 
    android:layout_height="70dp">

    <Button
        android:text="button"
        android:layout_width="100dp"
        android:layout_height="50dp" />
</LinearLayout>


c.根据上面的分析,只要ViewGroup的child的hasFocusable()都是返回false的,那么ViewGroup的 hasFocusable()就会返回false.

所以可以这样设置


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="70dp">

    <Button
        android:text="button"
        android:focusable="false" // 设置focusable为false 如果有多个button,同样要全部设置。
        android:layout_width="100dp"
        android:layout_height="50dp" />
</LinearLayout>


以上纯属个人分析得出的结论,难免有纰漏,如有错误,请吐槽。

















  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值