如下一个简单示例
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>
所以可以这样设置
<?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>
以上纯属个人分析得出的结论,难免有纰漏,如有错误,请吐槽。