你的位置:首页 > 操作系统

[操作系统]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了。

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

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