Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 中ListView setOnItemClickListener點擊無效原因分析

Android 中ListView setOnItemClickListener點擊無效原因分析

編輯:關於Android編程

前言

最近在做項目的過程中,在使用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;
}

看源碼我們可以知道:

如果 ViewGroup visiable 和 focusable 都為 true,就算能夠獲取焦點, 返回 true。
如果我們給ViewGroup設置了descendantFocusability屬性,並且等於FOCUS_BLOCK_DESCENDANTS的情況下,返回false。不能獲取焦點。
如果沒有設置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();
}

在觸摸模式下如果不可獲取焦點,先遍歷 View 的所有父節點,如果有一個父節點設置了阻塞子 View 獲取焦點,那麼該 View 就不可能獲取焦點
在觸摸模式下如果不可獲取焦點,並且沒有父節點設置阻塞子 View 獲取焦點,和在觸摸模式下如果可以獲取焦點,那麼才判斷 View 自身的 visiable 和 focusable 屬性,來決定是否可以獲取焦點,只有 visiable 和 focusable 同時為 true,該View 才可能獲取焦點。

好了,分析到這裡我們再回過頭去看兩個解決辦法。

在checkbox、button對應的view處加android:focusable=”false”

復制代碼 代碼如下:
android:clickable=”false” android:focusableInTouchMode=”false”

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

以上所述是本文給大家分享的Android 中ListView setOnItemClickListener點擊無效原因分析,希望大家喜歡。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved