ListView setOnItemClickListener无效原因分析

https://blog.csdn.net/weixin_30784945/article/details/98094246

前言

最近在做项目的过程中,在使用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去找。还真有:


 
 
  1. @Override
  2. public boolean onTouchEvent(MotionEvent ev) {
  3. if (!isEnabled()) {
  4. // A disabled view that is clickable still consumes the touch
  5. // events, it just doesn't respond to them.
  6. return isClickable() || isLongClickable();
  7. }
  8. if (mPositionScroller != null) {
  9. mPositionScroller.stop();
  10. }
  11. if (mIsDetaching || !isAttachedToWindow()) {
  12. // Something isn't right.
  13. // Since we rely on being attached to get data set change notifications,
  14. // don't risk doing anything where we might try to resync and find things
  15. // in a bogus state.
  16. return false;
  17. }
  18. startNestedScroll(SCROLL_AXIS_VERTICAL);
  19. if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
  20. return true;
  21. }
  22. initVelocityTrackerIfNotExists();
  23. final MotionEvent vtev = MotionEvent.obtain(ev);
  24. final int actionMasked = ev.getActionMasked();
  25. if (actionMasked == MotionEvent.ACTION_DOWN) {
  26. mNestedYOffset = 0;
  27. }
  28. vtev.offsetLocation( 0, mNestedYOffset);
  29. switch (actionMasked) {
  30. case MotionEvent.ACTION_DOWN: {
  31. onTouchDown(ev);
  32. break;
  33. }
  34. case MotionEvent.ACTION_MOVE: {
  35. onTouchMove(ev, vtev);
  36. break;
  37. }
  38. case MotionEvent.ACTION_UP: {
  39. onTouchUp(ev);
  40. break;
  41. }
  42. case MotionEvent.ACTION_CANCEL: {
  43. onTouchCancel();
  44. break;
  45. }
  46. case MotionEvent.ACTION_POINTER_UP: {
  47. onSecondaryPointerUp(ev);
  48. final int x = mMotionX;
  49. final int y = mMotionY;
  50. final int motionPosition = pointToPosition(x, y);
  51. if (motionPosition >= 0) {
  52. // Remember where the motion event started
  53. final View child = getChildAt(motionPosition - mFirstPosition);
  54. mMotionViewOriginalTop = child.getTop();
  55. mMotionPosition = motionPosition;
  56. }
  57. mLastY = y;
  58. break;
  59. }
  60. case MotionEvent.ACTION_POINTER_DOWN: {
  61. // New pointers take over dragging duties
  62. final int index = ev.getActionIndex();
  63. final int id = ev.getPointerId( index);
  64. final int x = ( int) ev.getX( index);
  65. final int y = ( int) ev.getY( index);
  66. mMotionCorrection = 0;
  67. mActivePointerId = id;
  68. mMotionX = x;
  69. mMotionY = y;
  70. final int motionPosition = pointToPosition(x, y);
  71. if (motionPosition >= 0) {
  72. // Remember where the motion event started
  73. final View child = getChildAt(motionPosition - mFirstPosition);
  74. mMotionViewOriginalTop = child.getTop();
  75. mMotionPosition = motionPosition;
  76. }
  77. mLastY = y;
  78. break;
  79. }
  80. }
  81. if (mVelocityTracker != null) {
  82. mVelocityTracker.addMovement(vtev);
  83. }
  84. vtev.recycle();
  85. return true;
  86. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98

代码比较长,我们主要看46行 MotionEvent.ACTION_UP的情况,因为onItemClick事件的触发是在我们的手指从屏幕抬起的那一刻,在MotionEvent.ACTION_UP的情况下执行了onTouchUp(ev);那么我们可以想到问题发生的原因应该就是在这个方法了里了。


 
 
  1. private void onTouchUp (MotionEvent ev) {
  2. switch (mTouchMode) {
  3. case TOUCH_MODE_DOWN:
  4. case TOUCH_MODE_TAP:
  5. case TOUCH_MODE_DONE_WAITING:
  6. final int motionPosition = mMotionPosition;
  7. final View child = getChildAt(motionPosition - mFirstPosition);
  8. if (child != null) {
  9. if (mTouchMode != TOUCH_MODE_DOWN) {
  10. child.setPressed( false);
  11. }
  12. final float x = ev.getX();
  13. final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
  14. if (inList && !child.hasFocusable()) {
  15. if (mPerformClick == null) {
  16. mPerformClick = new PerformClick();
  17. }
  18. final AbsListView.PerformClick performClick = mPerformClick;
  19. performClick.mClickMotionPosition = motionPosition;
  20. performClick.rememberWindowAttachCount();
  21. mResurrectToPosition = motionPosition;
  22. if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
  23. removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
  24. mPendingCheckForTap : mPendingCheckForLongPress);
  25. mLayoutMode = LAYOUT_NORMAL;
  26. if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
  27. mTouchMode = TOUCH_MODE_TAP;
  28. setSelectedPositionInt(mMotionPosition);
  29. layoutChildren();
  30. child.setPressed( true);
  31. positionSelector(mMotionPosition, child);
  32. setPressed( true);
  33. if (mSelector != null) {
  34. Drawable d = mSelector.getCurrent();
  35. if (d != null && d instanceof TransitionDrawable) {
  36. ((TransitionDrawable) d).resetTransition();
  37. }
  38. mSelector.setHotspot(x, ev.getY());
  39. }
  40. if (mTouchModeReset != null) {
  41. removeCallbacks(mTouchModeReset);
  42. }
  43. mTouchModeReset = new Runnable() {
  44. @Override
  45. public void run () {
  46. mTouchModeReset = null;
  47. mTouchMode = TOUCH_MODE_REST;
  48. child.setPressed( false);
  49. setPressed( false);
  50. if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
  51. performClick.run();
  52. }
  53. }
  54. };
  55. postDelayed(mTouchModeReset,
  56. ViewConfiguration.getPressedStateDuration());
  57. } else {
  58. mTouchMode = TOUCH_MODE_REST;
  59. updateSelectorState();
  60. }
  61. return;
  62. } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
  63. performClick.run();
  64. }
  65. }
  66. }
  67. mTouchMode = TOUCH_MODE_REST;
  68. updateSelectorState();
  69. break;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

这里主要看7行到18行,拿到了我们item的View,并且在15行代码里判断了item的View是否在范围是否获取焦点(hasFocusable()),这里对hasFocusable()取反判断,也就是说,必需要我们的itemView的hasFocusable() 方法返回false, 才会执行一下的方法,以下的方法就是点击事件的方法。那么我们来看看是不是mPerformClick真的就是执行我们的itemClick事件。

PerformClick以及相关代码如下:


 
 
  1. private class PerformClick extends WindowRunnnable implements Runnable {
  2. int mClickMotionPosition;
  3. @Override
  4. public void run () {
  5. // The data has changed since we posted this action in the event queue,
  6. // bail out before bad things happen
  7. if (mDataChanged) return;
  8. final ListAdapter adapter = mAdapter;
  9. final int motionPosition = mClickMotionPosition;
  10. if (adapter != null && mItemCount > 0 &&
  11. motionPosition != INVALID_POSITION &&
  12. motionPosition < adapter.getCount() && sameWindow()) {
  13. final View view = getChildAt(motionPosition - mFirstPosition);
  14. // If there is no view, something bad happened (the view scrolled off the
  15. // screen, etc.) and we should cancel the click
  16. if (view != null) {
  17. performItemClick(view, motionPosition, adapter.getItemId(motionPosition));
  18. }
  19. }
  20. }
  21. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

第18行代码拿到了我们点击的item View,并且调用了performItemClick方法。我们再来看absListView的performItemClick方法:


 
 
  1. @ Override
  2. public boolean performItemClick (View view, int position, long id) {
  3. boolean handled = false;
  4. boolean dispatchItemClick = true;
  5. if (mChoiceMode != CHOICE_MODE_NONE) {
  6. handled = true;
  7. boolean checkedStateChanged = false;
  8. if (mChoiceMode == CHOICE_MODE_MULTIPLE ||
  9. (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) {
  10. boolean checked = !mCheckStates. get(position, false);
  11. mCheckStates.put(position, checked);
  12. if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
  13. if ( checked) {
  14. mCheckedIdStates.put(mAdapter.getItemId(position), position);
  15. } else {
  16. mCheckedIdStates.delete(mAdapter.getItemId(position));
  17. }
  18. }
  19. if ( checked) {
  20. mCheckedItemCount++;
  21. } else {
  22. mCheckedItemCount--;
  23. }
  24. if (mChoiceActionMode != null) {
  25. mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
  26. position, id, checked);
  27. dispatchItemClick = false;
  28. }
  29. checkedStateChanged = true;
  30. } else if (mChoiceMode == CHOICE_MODE_SINGLE) {
  31. boolean checked = !mCheckStates. get(position, false);
  32. if ( checked) {
  33. mCheckStates.clear();
  34. mCheckStates.put(position, true);
  35. if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
  36. mCheckedIdStates.clear();
  37. mCheckedIdStates.put(mAdapter.getItemId(position), position);
  38. }
  39. mCheckedItemCount = 1;
  40. } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt( 0)) {
  41. mCheckedItemCount = 0;
  42. }
  43. checkedStateChanged = true;
  44. }
  45. if (checkedStateChanged) {
  46. updateOnScreenCheckedViews();
  47. }
  48. }
  49. if (dispatchItemClick) {
  50. handled |= super.performItemClick(view, position, id);
  51. }
  52. return handled;
  53. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

看第54行调用了父类的performItemClick方法:


 
 
  1. public boolean performItemClick (View view, int position, long id) {
  2. final boolean result;
  3. if (mOnItemClickListener != null) {
  4. playSoundEffect(SoundEffectConstants.CLICK);
  5. mOnItemClickListener.onItemClick( this, view, position, id);
  6. result = true;
  7. } else {
  8. result = false;
  9. }
  10. if (view != null) {
  11. view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  12. }
  13. return result;
  14. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

好了,搞了半天,终于到点上了。第3

行代码很明显了,就是如果有ItemClickListener,就执行他的onItemClick方法,最终回调到我们常见的那个方法。

到这里,相信大家已经知道,关键代码就是刚才上面我们分析的那一个if判断


 
 
  1. if (inList && !child .hasFocusable()) {
  2. if (mPerformClick == null) {
  3. mPerformClick = new PerformClick();
  4. }
  5. ... ..
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

也就是只有item的View hasFocusable( )方法返回false,才会执行onItemClick。

View 和 ViewGroup 的 hasFocusable

ViewGroup的hasFocusable

源码


 
 
  1. @Override
  2. public boolean hasFocusable () {
  3. if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
  4. return false;
  5. }
  6. if (isFocusable()) {
  7. return true;
  8. }
  9. final int descendantFocusability = getDescendantFocusability();
  10. if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
  11. final int count = mChildrenCount;
  12. final View[] children = mChildren;
  13. for ( int i = 0; i < count; i++) {
  14. final View child = children[i];
  15. if (child.hasFocusable()) {
  16. return true;
  17. }
  18. }
  19. }
  20. return false;
  21. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

看源码我们可以知道:

  1. 如果 ViewGroup visiable 和 focusable 都为 true,就算能够获取焦点, 返回 true。
  2. 如果我们给ViewGroup设置了descendantFocusability属性,并且等于FOCUS_BLOCK_DESCENDANTS的情况下,返回false。不能获取焦点。
  3. 如果没有设置descendantFocusability属性的话,只要一个子View hasFocusable返回了true,ViewGroup的hasFocusable就返回。

再来看View的hasFocusable

ViewGroup的hasFocusable


 
 
  1. public boolean hasFocusable () {
  2. if (!isFocusableInTouchMode()) {
  3. for (ViewParent p = mParent; p instanceof ViewGroup; p = p.getParent()) {
  4. final ViewGroup g = (ViewGroup) p;
  5. if (g.shouldBlockFocusForTouchscreen()) {
  6. return false;
  7. }
  8. }
  9. }
  10. return (mViewFlags & VISIBILITY_MASK) == VISIBLE && isFocusable();
  11. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  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也没有获取焦点,那么上面说到回调至性的条件判断也就的代码:


 
 
  1. if (inList && !child .hasFocusable()) {
  2. if (mPerformClick == null) {
  3. mPerformClick = new PerformClick();
  4. }
  5. ... ..
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

if条件成立,所有执行了回调。

第二种情况,item,设置了descendantFocusability=”blocksDescendants”,所有没有遍历子 View,child.hasFocusable()直接返回false了。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值